React 18 업그레이드 후 useEffect 두 번 실행 문제

문제 상황

지난주 React 18로 업그레이드를 진행했다. 배포 후 개발 환경에서 API 호출이 두 번씩 발생하는 이슈를 발견했다.

useEffect(() => {
  fetchUserData(userId);
}, [userId]);

개발 서버에서 이 코드가 마운트 시 두 번 실행되면서 불필요한 네트워크 요청이 발생했다.

원인 분석

React 18의 Strict Mode가 컴포넌트를 의도적으로 마운트-언마운트-재마운트하는 동작을 추가했다. 향후 도입될 기능들(Offscreen API 등)을 대비한 변경사항이었다.

공식 문서를 확인하니 개발 환경에서만 발생하는 의도된 동작이었다. 프로덕션에는 영향이 없다.

해결 방법

1. Cleanup 함수 추가

useEffect(() => {
  let cancelled = false;
  
  fetchUserData(userId).then(data => {
    if (!cancelled) {
      setUser(data);
    }
  });
  
  return () => {
    cancelled = true;
  };
}, [userId]);

2. AbortController 활용

useEffect(() => {
  const controller = new AbortController();
  
  fetch(`/api/users/${userId}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(setUser)
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });
  
  return () => controller.abort();
}, [userId]);

교훈

React 18 이전에도 cleanup 함수를 작성하는 게 권장사항이었지만, 실제로 제대로 구현하지 않은 코드가 많았다. 이번 변경으로 side effect 관리의 중요성을 다시 느꼈다.

팀 내 다른 프로젝트들도 비슷한 이슈가 있을 것으로 예상되어 가이드 문서를 작성해 공유했다.