[React] React 18버전 이후, useEffect가 두 번 호출되는 이유와 해결방안
1. 문제 직면
`리액트를 다루는 기술` 책으로 리액트 공부를 하다가 코드를 똑같이 작성했음에도 프로젝트가 오작동 하는 것을 발견했다. 게시글을 수정하기 위해, write state에 기존 게시글 정보를 담고, 게시글 작성 페이지로 이동하도록 구현하였다. 하지만 게시글 정보가 계속 초기화 되었다. 리덕스 개발 툴로 확인해 보니, 게시글 수정 페이지가 unmount 될 때 write state를 초기화하는 함수가 호출되는 것이었다. 언마운트는 일어나지 않았음에도 호출되는게 이상하여 디버깅 해보니, mount -> unmount -> mount가 되고 있었다.
해당 문제에 관하여 구글링 하니 React 18버전 이후에는 컴포넌트가 두 번 호출된다는 것이었다.
2. 문제 원인
구버전에는 컴포넌트가 오직 한 번만 mount 되도록 설계되었다. 하지만 18버전인 현재(2022년)는 Hook을 사용할 경우 두 번 호출되도록 설계되었다. (Strict Mode로 작업할 경우)
유튜브에서 참고한 개발자의 말을 첨부하자면,
react 18 : mount -> unmount -> mount ------------------------> unmount
react 18 이후 : mount -> umount --...--> mount -------------> unmount
현재에는 그저 mount가 연속으로 두 번 mount 되지만, 미래에는 상태를 유지하면서 여러 번 mount 할 수 있도록 기능을 추가할 것 같다고 하였다.
React 18에서는 추후 React 상태를 유지하면서 UI 섹션을 추가 및 제거할 수 있는 기능을 추가하고 싶습니다. 예를 들어, 사용자가 화면에서 탭으로 뒤로 이동할 때 React는 즉시 이전 화면을 표시할 수 있어야 합니다. 이를 위해 React는 이전과 동일한 구성 요소 상태를 사용하여 트리를 마운트 해제하고 다시 마운트합니다.
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React would unmount and remount trees using the same component state as before.
Strict Mode를 사용하면 예상치 못한 부작용을 찾을 수 있도록 React가 컴포넌트를 두 번 렌더링합니다. React 17에서는 로그를 더 쉽게 읽을 수 있도록 두 렌더링 중 하나에 대한 콘솔 로그를 출력하지 않았습니다. 그러나 이러한 방침이 더 혼란스럽다는 내부의 의견에 따라 우리는 콘솔 로그를 다 출력하도록 했습니다. 대신, React DevTools가 설치되어 있으면, 두 번째 로그의 렌더링이 회색으로 표시되고, 이 렌더링을 완전히 억제하는 옵션이 기본적으로 해제됩니다.
No suppression of console logs: When you use Strict Mode, React renders each component twice to help you find unexpected side effects. In React 17, we've suppressed console logs for one of the two renders to make the logs easier to read. In response to community feedback about this being confusing, we've removed the suppression. Instead, if you have React DevTools installed, the second log's renders will be displayed in grey, and there will be an option (off by default) to suppress them completely.
React 공식 문서에 따르면 위와 같이 설명되어있다. 17에서도 두 번씩 호출되는 것 같은데, 콘솔 로그에는 하나의 렌더링에 대한 콘솔만 출력하도록 했었나보다. 18인 지금은 콘솔에 두 개가 다 출력되는 것을 확인할 수 있다.
3. 해결 방법
(1) useEffectOnce 권장x
useEffectOnce라는 custom Hook을 사용하면 해결된다고 한다. 그러나 권장하진 않는다고 한다.
(이 Hook에 대해서는 알아보지 않아서 잘 모르겠습니다. 추후에 혹시라도 알아보게 된다면 추가 작성 하겠습니다.)
(2) flag 설정
컴포넌트가 처음 mount 되었을 때, flag를 설정하여 두 번째 mount 되었을 때 코드를 두 번 호출하는 것을 방지한다.
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
console.log('mounted!!')
}, []);
일반 변수로 선언하게 되면, 컴포넌트가 리마운트 될 때마다 초기화되므로 의미가 없다. useRef를 사용하여 변수를 레퍼런스 하고, 이미 한 번 마운트 되었다면 코드를 실행하지 않도록 설정하면 된다.
하지만 단점이 하나 있는데, 위 코드는 clean up 코드를 포함할 수 없다.
clean up을 사용하려면 상황에 따라 적절하게 코드를 바꾸어야 한다.
// 언마운트 될 때 초기화
const mounted = useRef(false);
useEffect(() => {
return () => {
if (mounted.current) dispatch(initialize());
mounted.current = true;
};
}, [dispatch]);
나와 같은 경우에는 위처럼, unmount될 때만 다루면 되므로, 두 번째 unmount일 경우에만 호출되도록 하였다.