src>extensions>React>exhaustive-deps.mdx

useEffect의 의존성 배열 문제

v1.0.0
@young_log|React|Published on 2026-01-06

"하라는거 다 했는데 왜 무한로딩?"

@
Details
Changelog
Dependencies

exhaustive-deps란?

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }
 
  useEffect(() => {
    doSomething();
  }, []); // 🔴 이것은 안전하지 않다 (`someProp`을 사용하는`doSomething`을 호출)
  // 그러면 'doSomething'를 useEffect 안에 넣어라
}

image.png

React Hook useEffect has missing dependencies: ‘…’, … Either include them or remove the dependency array.

해당 데이터들이 다 바뀌었는데 useEffect 내부의 데이터는 다 최신 상태여야 한다는게 리액트의 철학 때문에
useEffect가 안돌아가면 얘네들은 오래된 데이터가 될텐데 괜찮은가?하고 에러 내보내는 것이다.

eslint-plugin-react-hooks 패키지의 일부로 exhaustive-deps ESLint 규칙을 제공하는데,
ESLint로 인해 의존성을 철저하게(exhaustive) 다 적었는지 검사를 해준다.

useEffect의 의존성을 비워두면 처음 한 번만 렌더링되기 때문에 처음의 데이터만 기억하고 있는 상태임. → 데이터 불일치

그런데 의존성을 다 넣어주게 되면 무한루프가 도는 경우가 있다.


왜 무한루프가 도는 것인지?

내가 넣은 의존성이 사실은 실행될 때마다 ‘New’로 변하고 있다.

  1. 렌더링: 리액트가 컴포넌트를 그린다. 이때 getSummary 함수가 새로 생성된다.(내용은 같지만 메모리 주소가 바뀜)
  2. useEffect 감지: getSummary가 바뀌었네? → 재실행
  3. 함수 실행: getSummary 안에서 데이터 가져와서 setData(결과) 호출
  4. 상태 변경: setData가 호출되었으니 리액트다 다시 리렌더링
  5. 무한 반복 …

이름이 같은 함수인데 왜 재실행되는 것일까?

리액트 입장에서는 컴포넌트가 돌 때마다 이름만 같은 다른 함수라고 판단(자바스크립트의 객체/함수 특성 때문)

  1. 자바스크립트의 참조값 특성

    자바스크립트에서는 함수나 객체는 메모리 주소를 갖는다.

    • 숫자나 문자열(원시 타입): 1은 언제나 1, “Young”는 언제나 “Young”이다.
    • 함수나 객체(참조 타입): 겉모양이 똑같이 생겼어도 메모리에 저장된 위치(주소)가 다르면 완전히 다른 객체로 본다.
    // 겉보기엔 똑같은 함수지만
    const func1 = () => console.log("안녕");
    const func2 = () => console.log("안녕");
     
    console.log(func1 === func2); // 결과는 false! (주소가 다르니까요)
  2. 컴포넌트 렌더링 = 함수 재실행 (지역함수)

    리액트 컴포넌트도 함수인데, 상태가 바뀌어 렌더링이 일어났다는 것은 이 함수가 재실행되었다는 것!

    1. App 이라는 큰 함수 실행
    2. 그 안의 const로 지역 함수 재선언
    3. 새로운 메모리 공간 할당하여 함수 저장
    4. 이전 렌더링이랑 달라진 메모리에 저장된 주소로 인해 재실행된다.
  3. 리액트의 비교 방식

    리액트는 useEffect 의존성 배열 검사시, 엄격한 비교(===) 사용

    • 주소가 바뀌면 데이터가 바뀐 거로 판단.

해결 방법

useEffectuseLayoutEffectuseMemouseCallback , useImperativeHandle

위와 같은 훅에서 마지막 인수로 종속성 목록을 지정하는데, 콜백 내에서 사용되는 모든 값을 포함하고 리액트 데이터 흐름에 참여되어야 한다.
state, prop, 함수 등등

  1. 종속되는 거 다 적어주기

    function Example({ someProp }) {
      function doSomething() {
        console.log(someProp);
      }
      useEffect(() => {
        doSomething();
      }, [someProp]);
    }
  2. useEffect 안으로 이동 시키기

    function Example({ someProp }) {
      useEffect(() => {
        // 밖에 있던 함수를 안으로 이사 시켰어요!
        function doSomething() {
          console.log(someProp);
        }
        doSomething();
      }, [someProp]); // 이제 someProp이 바뀔 때마다 이 함수가 최신 데이터를 들고 실행돼요.
    }

    함수가 useEffect 밖에 있으면 그 함수가 내부에서 뭘 쓰는지 리액트가 알 수 없다.
    안에 있으면 someProp를 쓴다는걸 알수 있다.

  3. useCallback 으로 포장 시키기

    const doSomething = useCallback(() => {
      console.log(someProp);
    }, [someProp]); // someProp이 바뀔 때만 함수가 새로 만들어짐
     
    useEffect(() => {
      doSomething();
    }, [doSomething]); // 함수의 '정체성'이 바뀌었을 때만 실행!

    그럼에도 함수를 밖에 유지하고 싶다면?(useCallback)

    useCallback 안에 함수 자체를 의존성 배열에 넣을 수 있게 포장하기!

    진짜 중요한 데이터가 바뀔 때만 새로 만들고, 평소에는 재활용되어진다.
    → 리액트가 메모리에 이 함수의 주소를 저장해두고, 리렌더링해도 새로 주소를 만들지 않고 그 주소를 그대로 사용!!)

  4. 경고 무시하기 (권장 안함)

    // eslint-disable-next-line

    의존성 종속을 하지 않는 것 자체가 “리액트의 엔진 작동 원리를 망가뜨리는 것”

    1. 오래된 데이터(유령 버그)
    2. 리액트의 자동 동기화 무시

결론

useEffect는 동기화 장치이다. → 데이터가 변하면 이펙트도 반응해야 한다.
이펙트 안에서 쓰는 모든 “리액트 값(props, state, 함수)”는 의존성 배열에 반드시 포함하기
클로저 문제로 인해 옛날 데이터를 보여주게 되는 버그, 무한 루프 막을 수 있다.

참고 내용

https://bobbyhadz.com/blog/react-hooks-exhaustive-deps

https://ko.legacy.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

Comments_Log
TERMINAL
DEBUG CONSOLE
OUTPUT
~/stay-young-loggit(main)npm run comment:write
nickname:
content:
-- TOTAL COMMENTS: 0 --
[LOADING...] fetching data from supabase...