| 언어 | Java 17, JavaScript (ES6+) |
| 프레임워크 | Spring Boot 3.3.3, React 18.3.1 |
| DB | MySQL 8.x |
| 빌드 도구 | Gradle 7.x |
제목은 참 난해해 보이지만 상황은 간단하다.
리액트 컴포넌트를 만들던 와중에 테이블태그를 사용할 일이 있었고, 총 4개의 <tr> 그룹으로 이루어진 테이블이었다.

예를들면 이런 식이다.
function SomeManage() {
const SomeRef = useRef();
const { handleSubmit } = useForm();
const [SomeIdCd, setSomeIdCd] = useState([]);
const [mvInoutDiv, setMvInoutDiv] = useState([]);
const [sexCd, setSexCd] = useState([]);
const [gnrtCd, setGnrtCd] = useState([]);
let defaultSexList = [];
let defaultGnrtList = [];
let defaultMvList = [];
const setDefaultList = (targetList, codeType, codeKey) => {
getCommonCodeList(codeType).then((data) => {
targetList.push(
...data.map((item) => ({
[codeKey]: item.code,
name: item.name,
}))
);
});
};
setDefaultList(defaultSexList, "SEX", "sex");
setDefaultList(defaultGnrtList, "GNRT", "gnrt");
setDefaultList(defaultMvList, "MV_INOUT_DIV", "mvInoutDiv");
const [mvList, setMvList] = useState(defaultMvList);
const [sexList, setSexList] = useState(defaultSexList);
const [gnrtList, setGnrtList] = useState(defaultGnrtList);
const [totalList, setTotalList] = useState([defaultTotalList]);
useEffect(() => {
setCommonCodeState("CLNT_CODE", setClntIdCd);
setCommonCodeState("MV_INOUT_DIV", setMvInoutDiv);
setCommonCodeState("SEX", setSexCd);
setCommonCodeState("GNRT", setGnrtCd);
setTotalList(defaultTotalList);
search();
}, []);
...
위 소스는 해당 페이지의 state 초기화 및 데이터 설정 로직의 일부분이다.
간략한 설명을 하자면 DB에서 이사여부, 성별 등의 공통코드를 한 테이블에서 관리하고 있고
setDefaultList()는 서버에서 데이터를 가져오기 전에 메타데이터를 공통코드테이블에서 미리 가져와서 화면을 구성하기 위한 용도의 함수이다.
setCommonCodeState(), getCommonCodeList() 는 API를 호출해 해당 코드와 코드명을 매칭시켜주는 공통모듈 비동기 함수이다.
해당화면의 요구사항은 데이터의 유무 관계없이 화면 렌더링과 동시에 테이블의 좌측에 해당 열의 구분을 쏴줘야 하는 것 이었다.

이런식으로
남자, 여자, 10대, 20대 이런 열 구분이 페이지 로드와 동시에 렌더링 되어야 한다
여기서 문제가 발생했다.
'이사여부' 관련 <tr>블록이 렌더링 되지 않는 것이었다. (정확히는 될 때도 있고, 안될 때도 있었다)
나머지 '성별', '연령대', '합계'도 마찬가지로 렌더링이 되지 않을 때가 있었다.
새로고침을 누르면 해결되긴 했으나 이건 명백한 오류였다.
현재까지 주어진 정보로 위 소스만 보고 이유를 찾을 수 있겠는가?
나는 먼저 비동기 함수의 실행 순서를 의심했다.
setDefaultList(defaultSexList, "SEX", "sex");
setDefaultList(defaultGnrtList, "GNRT", "gnrt");
setDefaultList(defaultMvList, "MV_INOUT_DIV", "mvInoutDiv");
이렇게 되어있던 setDefaultList() 함수 호출 순서를
setDefaultList(defaultMvList, "MV_INOUT_DIV", "mvInoutDiv");
setDefaultList(defaultSexList, "SEX", "sex");
setDefaultList(defaultGnrtList, "GNRT", "gnrt");
이렇게 열구분 순서에 맞게 바꾸었더니 문제가 현저히 줄었다.
순서를 바꾼 것만으로 차이가 있었던 이유는 비동기 작업의 실행 타이밍이 달라졌기 때문이다.
setDefaultList 함수는 데이터를 가져오는 비동기 작업(getCommonCodeList)을 수행한다. 이 작업이 완료되기 전에 화면이 렌더링되면서 초기화되지 않은 데이터를 참조해 화면이 제대로 보이지 않았던 것.
defaultSexList부터 비동기 작업이 시작되는데 이후 defaultGnrtList, defaultMvList의 작업이 차례로 호출되지만 각 작업의 완료 시점은 실행 순서와 다를 수 있었다. 수정 전 코드로는 defaultMvList가 렌더링 시점에 비어 있을 가능성이 컸던 것이다.
수정후 defaultMvList의 비동기 작업이 가장 먼저 실행되고, 렌더링 시점에 defaultMvList의 데이터가 초기화될 가능성이 높아져서 정상적으로 동작할 수 있었다.
함수 호출 순서를 변경함으로써 운 좋게(?) 비동기 함수의 실행 완료 순서는 호출 순서에 따라 다를 수 있다는 것을 알게 되었다.
다시한번 핵심을 정리하면,
비동기 함수 자체는 호출 순서에 상관없이 실행 타이밍이 독립적으로 이루어진다. 즉, 비동기 함수의 실행 순서와 완료 순서는 다를 수 있다.
하지만 비동기 함수 호출 순서가 중요한 경우가 있는데,
1) 데이터 의존성이 있을 때
2) 렌더링 타이밍이 중요한 경우
이런경우이다.
즉, 어떤 비동기 함수의 결과를 다른 함수에서 사용해야 하는 경우, 또는 React에서 특정 비동기 데이터가 준비된 후 상태를 업데이트하고 이를 기반으로 컴포넌트를 렌더링해야 하는 경우, 호출 순서와 완료 순서를 명확히 해야 한다.
이제 근본적인 문제 해결을 해보자.
function SomeManage() {
const searchRef = useRef();
const { handleSubmit } = useForm();
const [SomeCd, setSomeCd] = useState([]);
const [mvInoutDiv, setMvInoutDiv] = useState([]);
const [sexCd, setSexCd] = useState([]);
const [gnrtCd, setGnrtCd] = useState([]);
let defaultMvList = [];
let defaultSexList = [];
let defaultGnrtList = [];
const setDefaultList = (targetList, codeType, codeKey) => {
return getCommonCodeList(codeType).then((data) => {
targetList.push(
...data.map((item) => ({
[codeKey]: item.code,
name: item.name,
}))
);
});
};
const [mvList, setMvList] = useState(defaultMvList);
const [sexList, setSexList] = useState(defaultSexList);
const [gnrtList, setGnrtList] = useState(defaultGnrtList);
const [totalList, setTotalList] = useState([defaultTotalList]);
useEffect(() => {
initialize();
}, []);
const initialize = async () => {
await Promise.all([
setDefaultList(defaultMvList, "MV_INOUT_DIV", "mvInoutDiv"),
setDefaultList(defaultSexList, "SEX", "sex"),
setDefaultList(defaultGnrtList, "GNRT", "gnrt"),
]);
setTotalList(defaultTotalList);
search();
};
...
이건 문제를 해결한 최종 코드이다.
제일먼저 setCommonCodeState(), getCommonCodeList() 이 두 함수가 중복로직이라 하나를 제거했다.
그리고 useEffect에 setDefaultList 함수들을 위치시켰다. 페이지가 로드됨과 동시에 해당 함수들을 실행하기 위해서였다. 하지만 문제는 여전히 해결되지 않았었다.
그래서 Promise.all 를 사용했다. 모든 비동기 작업이 완료될 때까지 기다린 후 다음 줄로 넘어가도록 만들기 위함이다. search(); 는 최초에 데이터를 한번 뿌려주기 위해 넣어놨는데 이것보다 위에서 사용했다. 즉, 배열에 기본값이 완전히 세팅된 후에야 데이터를 불러온 것.
그리고 setDefaultList 함수가 데이터를 초기화한 후 Promise를 반환하도록 수정했다. 이를 통해 호출한 Promise.all이 각 작업의 완료 여부를 감지할 수 있었다.
순서를 정리하면 이렇다.
1) useEffect가 실행되면 initialize 함수가 호출됨
2) initialize는 Promise.all로 setDefaultList를 호출하여 defaultMvList, defaultSexList, defaultGnrtList를 병렬로 초기화 함
3) 이후 search를 호출하여 초기 데이터를 뿌림
4) 데이터가 완전히 준비된 후에 화면이 렌더링됨
이 작업은 하드코딩을 피하기 위함이기도 했다. 처음에는 서버에서 공통코드를 가져오지 않고 그냥 객체배열을 만들어서 썼었는데 유지보수 측면에서 매우 비효율적이라 저렇게 동적으로 가져오도록 바꿨다.
P.S. 다음날 대리님이 코드 읽기 힘들다고 싹 갈아엎었다...
'React' 카테고리의 다른 글
| [React] useReducer, memo, useMemo (0) | 2025.11.20 |
|---|---|
| [React] axios / fetch / Express의 app.get() 역할 구분 (0) | 2025.11.18 |
| React를 사용할 때 알면 좋은 훅 정리 (1) | 2025.09.01 |
| 리액트 - 차트 라이브러리 <월의 마지막 일자 계산법> [Recharts] (1) | 2024.12.16 |
| 리액트 - 그리드 라이브러리 기초사용법 예제 [@mui/x-data-grid-premium] (1) | 2024.12.12 |