front-end / / 2024. 2. 26. 10:16

[react] React Hook useEffect has a missing dependency는 왜 표시되는 걸까?

1. 문제상황

1) 문제 파악

React 개발 중에 아래와 같은 경고(warning)를 많이 만나봤을 것이다.

[eslint] 
src/App.js
  Line 11:6:  React Hook useEffect has a missing dependency: 'counter'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

WARNING in [eslint] 
src/App.js
  Line 11:6:  React Hook useEffect has a missing dependency: 'counter'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

webpack compiled with 1 warning

아래 예제를 통해 이런 경고가 왜 나타나는지 확인해 보자.

예제

아래 예제는 버튼 클릭 시 +1 숫자가 화면에 표시되는 간단한 앱이다.

function App() {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>+ Increment</button>
      <div>Count: {counter}</div>
    </div>
  );
}

여기서는 앱이 아무 문제없이 잘 실행된다.

여기에 useEffect를 사용해보자.

function App() {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    document.body.onclick = () => {
      console.log(counter);
    };
  }, []);

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>Click</button>
    </div>
  );
}

document.body.onclick를 추가하였고 body에 클릭하면 console.log로 counter를 출력하는 기능이다. 그리고 두번째 인수로 []을 넘겨 로딩 시 한번만 실행되도록 설정하였다.

저장을 하면 아래와 같은 경고가 출력이 된다.

[eslint] 
src/App.js
  Line 10:6:  React Hook useEffect has a missing dependency: 'counter'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

WARNING in [eslint] 
src/App.js
  Line 10:6:  React Hook useEffect has a missing dependency: 'counter'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

화면에서 기능을 동작을 해보자. 버튼(Click)을 눌러서 숫자가 잘 증가하는 지 확인해보자. (5번 클릭)

  1. 화면에 출력되는 Count는 5번 클릭한 대로 5라는 숫자가 표시되었다.
  2. 하지만 console.log에 찍힌 숫자는 0으로 표시가 되었다. (앞의 5는 동일한 값이 5번이라는 의미의 5이다.)

여기서 우리는 useState에서 사용되는 counter가 useEffect 내의 counter와 다르다는 것을 알 수 있다.

2) 원인 파악

문제를 해결하기 위한 방법을 한번 알아보자. 일단 메소드를 render하는 과정을 알아보기 위해 아래 그림을 한번 보자.

처음 클릭하는 경우는 왼쪽 (First Render) 그림이고 두 번째 클릭하는 경우는 오른쪽 그림(Second Render)이다. First render에서 클릭 시 counter라는 변수가 메모리 영역에 저장이 된다. Second render 시 counter라는 새로운 변수 영역에 할당이 된다. (기존 counter 변수가 변경되는 것이 아니다)
두 개의 counter는 이름이 같지만 다른 메모리 영역에 저장이 된다. 이것이 문제의 원인이 되는 것이다. 즉, 컴포넌트의 render가 발생할 때마다 새로운 변수가 할당된다는 것이다.

useEffect가 추가된 내용을 확인하기 위해 다음 그림을 한번 보자.

First render에서 counter 변수가 생성되고 useEffect 내부의 함수(onclick)가 생성이 된다. 여기서 생성된 함수는 0을 참조하고 있다. 이 함수는 항상 0이라는 값을 참조하게 된다. 사용자가 두 번째 클릭을 누르면 useEffect함수는 호출되지 않는다. 왜냐하면 두 번째 인수에 빈 배열을 넘겨주고 있기 때문이다. 그리고 counter 변수도 새로 생성된 메모리 영역에 생성이 된다. 다만 이름(counter)이 같을 뿐이다.

두 번째 클릭 시 console.log에 찍힌 counter는 first render에서 참조하는 counter를 바라 보기 때문에 항상 0 값을 보게 되는 것이다. 세 번째, 네 번째 모두 동일할 것이다.

3) 해결 방법

해결방법은 간단하다. useEffect에 의존성 배열에 counter를 넣어주면 된다.

function App() {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    document.body.onclick = () => {
      console.log(counter);
    };
  }, [counter]);

  return (
    <div>
      {counter}
      <button onClick={() => setCounter(counter + 1)}>Click</button>
    </div>
  );
}

2. 문제 상황 2

axios를 활용하여 api를 호출하는 문제상황을 한번 보자.

1) 문제 상황

위의 예제는 간단한 해결방법이었지만 메소드 내에서 axios를 활용하여 다른 api를 호출하는 예제를 한번 알아보자.

아래는 로딩 시 http://localhost:3001/books를 호출하여 api의 응답을 books에 적용하는 예제이다.

function App() {
  const { fetchBooks } = useContext(BooksContext);

  useEffect(() => {
    fetchBooks();
  }, []);

  return <div></div>;
}

BooksContext 내의 소스는 아래와 같다.

const BooksContext = createContext();

function Provider({ children }) {
  const [books, setBooks] = useState([]);

  const fetchBooks = async () => {
    const response = await axios.get("http://localhost:3001/books");
    setBooks(response.data);
  };
}

위의 코드도 아래와 같은 dependency 경고가 출력된다.

Line 23:6:  React Hook useEffect has a missing dependency: 'fetchBooks'. Either include it or remove the dependency array 

해결방법으로 fetchBooks를 의존성 배열에 추가해보자.

function App() {
  const { fetchBooks } = useContext(BooksContext);

  useEffect(() => {
    fetchBooks();
  }, [fetchBooks]);

  return <div></div>;
}

하지만 fetchBooks의 의존성 배열로 인해 아래와 같이 무한루프가 발생하고 있다.

2) 원인 파악

아래 그림을 보자.

First render에서 fetchBook이 실행되고 그 응답으로 books 이 변경이 된다. 그래서 re-render가 발생하고 fetchBooks의 새로운 메소드가 생성이 되어 useEffect 내부의 fetchBooks가 새로 호출이 된다. 이런 식으로 계속적으로 재귀호출이 발생하여 생기는 문제이다.

기능적으로는 정상적으로 동작을 하더라도 심각한 문제가 발생하는 것이다.

3) 해결 방법

이를 해결하는 방안으로 useCallback을 사용한다.

useCallback

  1. react에게 실제 변경되지 않는 함수를 알려주는 훅이다.
  2. useEffect와 관련된 버그 수정에 사용된다.
  3. useEffect와 사용법이 비슷하다. (두번째 인자는 배열)

Provider 내에 fetchBooks와 stableFetchBooks가 있다. stableFetchBooks는 useCallback을 사용한 메소드이다. stableFetchBooks 내의 fetchBooks는 first render 시 실행이 되지만 second render시에는 실행되지 않는다.

다시 설명하면 first render, second render fetchBooks는 매번 새로운 메소드가 생성이 되지만 stableFetchBooks는 first render 시 생성한 메소드가 재사용이 된다.

코드를 수정하면 아래와 같다.

  const fetchBooks = async () => {
    const response = await axios.get("http://localhost:3001/books");
    setBooks(response.data);
  };

  const stableFetchBooks = useCallback(fetchBooks, []);

여기서 fetchBooks 대신 stableFetchBooks를 사용하게 하면 된다. fetchBooks에 직접 useCallback을 적용해보자.

function Provider({ children }) {
    // ...
  const fetchBooks = useCallback(async () => {
    const response = await axios.get("http://localhost:3001/books");
    setBooks(response.data);
  }, []);
//    ...
}

이렇게 하면 fetchBooks는 한번 호출 후 재사용이 되는 구조를 갖게 되는 것이다.

참고

[https://www.udemy.com/course/react-redux/learn/lecture/34694870#overview]

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유