React useEffect 무한 루프 해결법과 의존성 배열 완벽 가이드

useEffect를 사용하다가 "React Hook useEffect is called conditionally" 에러를 마주하거나, 컴포넌트가 무한 리렌더링에 빠진 경험이 있다면 이 글은 필독입니다. 대부분의 문제는 의존성 배열(dependency array) 관리 실수훅 호출 규칙 위반에서 발생합니다. 이 글에서는 실무에서 자주 마주치는 useEffect 무한 루프 원인과 해결법, 그리고 의존성 배열을 정확히 관리하는 방법을 코드 예시와 함께 제시합니다.


React useEffect 무한 루프 해결법과 의존성 배열 완벽 가이드




1. useEffect 무한 루프가 발생하는 3가지 원인

useEffect가 무한 루프에 빠지는 이유는 대부분 의존성 배열에 매 렌더마다 새로 생성되는 값(객체, 배열, 함수)을 넣었기 때문입니다. React는 의존성 배열의 값을 이전 렌더링과 비교(얕은 비교, shallow comparison)하여 변경 여부를 판단하는데, 참조가 바뀌면 effect를 다시 실행합니다.

  • 객체/배열 리터럴을 의존성 배열에 직접 전달: useEffect(() => { ... }, [{ key: value }]) 형태로 작성하면 매 렌더마다 새 객체가 생성되어 참조가 달라지므로 effect가 무한 실행됩니다.
  • 함수를 의존성 배열에 포함: 컴포넌트 내부에서 선언한 함수는 렌더마다 새로 생성됩니다. 이를 의존성 배열에 넣으면 매번 effect가 재실행됩니다.
  • effect 내부에서 상태를 업데이트하고 해당 상태를 의존성 배열에 포함: useEffect(() => { setCount(count + 1); }, [count]) 패턴은 count 변경 → effect 실행 → count 변경 → effect 실행... 무한 루프를 유발합니다.

해결 전략:

  1. 객체/배열은 useMemo로 참조를 고정하거나, 필요한 원시값(primitive)만 의존성 배열에 넣으십시오.
  2. 함수는 useCallback으로 메모이제이션하거나, effect 내부에서 선언하여 의존성 배열에서 제외하십시오.
  3. 상태 업데이트 시 함수형 업데이트(setState(prev => prev + 1))를 사용하여 의존성 배열에서 상태를 제거하십시오.

2. 훅 호출 규칙 위반으로 인한 에러

useEffect를 조건문이나 반복문, 일반 함수 내부에서 호출하면 "React Hook useEffect is called conditionally" 에러가 발생합니다. React는 훅이 매 렌더링마다 동일한 순서로 호출되어야 내부 상태를 올바르게 추적할 수 있습니다.

잘못된 예시:

function MyComponent({ isPending }) {
  if (isPending) return <div>Loading...</div>;
  
  useEffect(() => {
    // 조건부 return 이후 훅 호출 → 에러 발생
  }, []);
}

올바른 예시:

function MyComponent({ isPending }) {
  useEffect(() => {
    // 훅을 최상단에 먼저 호출
  }, []);
  
  if (isPending) return <div>Loading...</div>;
}

추가 주의사항:

  • 컴포넌트 이름은 반드시 대문자로 시작: function cart()가 아닌 function Cart()로 작성해야 합니다. 소문자로 시작하면 React가 일반 함수로 인식하여 "is neither a React function component nor a custom React Hook function" 에러를 출력합니다.
  • ESLint 플러그인 활성화: eslint-plugin-react-hooksrules-of-hooks 규칙을 활성화하면 정적 분석 단계에서 훅 규칙 위반을 사전에 검출할 수 있습니다.

3. 의존성 배열(dependency array) 관리 전략

의존성 배열은 useEffect의 핵심입니다. 배열에 포함된 값이 변경될 때만 effect가 재실행되므로, effect가 의존하는 모든 값을 정확히 명시해야 합니다.

의존성 배열 동작 방식:

  • 생략: 매 렌더링마다 실행 (비권장, 성능 문제 유발)
  • 빈 배열 []: 컴포넌트 마운트 시 1회만 실행 (초기 데이터 로드, 이벤트 리스너 등록 등에 사용)
  • [value]: value가 변경될 때마다 실행 (마운트 시에도 실행됨)

실전 패턴:

  1. 초기 API 호출: useEffect(() => { fetchData(); }, []) — 빈 배열로 마운트 시 1회만 실행
  2. 특정 상태 변경 시 로직 실행: useEffect(() => { document.title = `Count: ${count}`; }, [count])
  3. 타이머 사용 시 cleanup 필수:
    useEffect(() => {
      const timer = setInterval(() => { ... }, 1000);
      return () => clearInterval(timer); // cleanup으로 메모리 누수 방지
    }, []);
        
  4. 함수/객체를 의존성으로 사용해야 할 때:
    const fetchUser = useCallback(() => { ... }, [userId]);
    useEffect(() => {
      fetchUser();
    }, [fetchUser]); // useCallback으로 참조 고정
        

주의: ESLint의 exhaustive-deps 규칙이 경고를 표시하면 무시하지 말고, 의존성을 추가하거나 effect 로직을 재구성하십시오. 의존성을 누락하면 stale closure 문제가 발생하여 예상치 못한 버그를 유발합니다.

4. 리렌더링 최적화와 useEffect의 관계

useEffect 무한 루프는 종종 불필요한 리렌더링과 연결되어 있습니다. 부모 컴포넌트가 리렌더될 때마다 자식에게 새로운 객체/함수를 props로 전달하면, 자식의 useEffect가 매번 실행되어 성능 문제를 일으킵니다.

최적화 도구:

  • React.memo: 자식 컴포넌트를 메모이제이션하여 props가 동일하면 리렌더를 건너뜁니다. const MemoChild = React.memo(Child);
  • useCallback: 함수 참조를 고정하여 자식에게 전달되는 함수 props로 인한 불필요한 리렌더를 방지합니다.
  • useMemo: 무거운 계산 결과나 객체 참조를 캐싱하여 의존성 배열에 안전하게 사용할 수 있습니다.

실무 예시:

// 나쁜 예: 매 렌더마다 새 객체 생성 → 자식 리렌더 → effect 재실행
<Child options={{ theme: 'dark' }} />

// 좋은 예: useMemo로 참조 고정
const options = useMemo(() => ({ theme: 'dark' }), []);
<Child options={options} />

권장 순서: 문제 인지 → React DevTools Profiler로 측정 → 병목 컴포넌트에 React.memo 적용 → 함수/객체 props는 useCallback/useMemo로 고정 → 재측정. 성능 체감이 없으면 조기 최적화는 오히려 코드 복잡도만 높입니다.

결론: useEffect 무한 루프 예방 체크리스트

useEffect 무한 루프와 의존성 배열 관리는 React 개발의 핵심 스킬입니다. 다음 3가지를 반드시 기억하십시오:

  1. 훅은 컴포넌트 최상단에서 무조건 호출 — 조건문, 반복문, 일반 함수 내부 호출 금지. 컴포넌트명은 대문자로 시작.
  2. 의존성 배열에는 effect가 의존하는 모든 값을 명시 — 객체/배열/함수는 useMemo/useCallback으로 참조 고정. 상태 업데이트 시 함수형 업데이트 사용.
  3. 타이머/이벤트 리스너는 cleanup 함수에서 반드시 정리 — 메모리 누수와 예기치 않은 동작을 방지.

Action Item: 지금 당장 프로젝트의 useEffect 코드를 열어 의존성 배열을 점검하고, ESLint의 react-hooks/exhaustive-deps 규칙을 활성화하십시오. 정적 분석 도구가 당신의 실수를 사전에 잡아줄 것입니다.


#함께 읽으면 좋은 글

HTTP 401과 403 에러의 결정적 차이와 올바른 사용법 : 바로보기

댓글