사내 프로젝트를 CRA 기반에서 Vite 기반으로 migration 하는 과정에서, 분명 같은 코드인데 로컬 개발 환경의 실행 결과와 빌드한 결과물의 실행 결과가 나오는 이상한 현상이 있었다.
주로 useEffect나 useState, useReducer가 두 번씩 실행되는 현상이었다. 처음에는, 왜 이런 현상이 일어나는지 몰라서 어리둥절 하기만 했다.
도저히 안되겠다 싶어서 집에서 비슷한 프로젝트를 만들어 크롬 개발자 도구에서 디버거를 걸었다. 코드의 실행 흐름을(React 내부에서 어떤 실행 흐름을 가지는지 알 수 있었다...) 따라가다 보니, 'strictMode' 라는 단어가 눈에 띄었다. 잊은 게 있었다. 이전 프로젝트는 React의 strict mode를 '강제로 끈' 상태의 프로젝트였다는 것을... Vite로 migration을 진행하면서, 이 strict mode도 다시 생겨난 것이었다.
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
vite 명령어로 프로젝트를 생성했다면, React의 Strict Mode는 main.tsx 파일의 일종의 wrapper 컴포넌트로 위치한다. 위 코드에서 볼 수 있듯이 <App /> 컴포넌트를 감싸고 있는 형태다.
이렇게 되면 React 프로젝트의 개발 환경은 Strict Mode로 실행이 되며 해당 Strict Mode 컴포넌트가 감싸고 있는 모든 컴포넌트는 Strict Mode 검사가 이루어진다.
이 Strict Mode 실행 환경에서 내가 제일 난감했던 것은 이전에 문제 없이 실행이 되었던(문제 없이 실행된다고 믿었던) useReducer 훅이 제대로 작동하지 않는 현상이었다.
이 현상은 useReducer의 reducer에서 특정 구문이 실행될 때마다 한 번이 아닌 두 번씩 실행되는 현상이었다. 두 번씩 실행되기 때문에 내가 원하는 동작을 구현해 놓아도, 그 동작 이후에 다시 동작을 실행하여 결과적으로 초기값으로 되돌아가고 있었다.
이 에러의 근원을 찾기 위해 구글링을 했는데, 나와 비슷한 문제를 겪은 사람이 만든 Github 이슈가 있었다.
이 이슈의 내용과 댓글을 살펴 보니 Strict Mode에서 useReducer 훅이 두 번 실행되는 현상은 reducer가 '순수'하지 않기 때문이라는 걸 알 수 있었다.
여기서 '순수하지 않다'의 뜻은 쉽게 풀어보자면 reducer 내부 코드에서 관리되는 state를, state 그 자체로 조작하는 것을 말한다.
const counterReducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return state.count + 1;
case "DECREMENT":
return state.count - 1;
case "RESET":
return 0;
default:
break;
}
};
위와 같은 코드가 state를 그대로 조작하는 reducer 코드다. state에 그대로 1을 더하거나 1을 빼고 있다. 위 코드는 아래와 같이 수정하여 '순수'하게 만들 수 있다.
const counterReducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return {
...state,
count: state.count + 1
};
case "DECREMENT":
return {
...state,
count: state.count - 1
};
case "RESET":
return { count: 0 };
default:
break;
}
};
위의 코드처럼 reducer를 순수한 함수로 만드려면, 조작한 state 값을 완전히 다시 할당해주어야 한다. 이렇게 코드를 바꾸고 나니 useReducer가 더는 두 번 실행되지 않았다.