개발새발 로그

useEffect에서 의존성 배열의 경고를 무시하지마..! 본문

React

useEffect에서 의존성 배열의 경고를 무시하지마..!

이즈흐 2024. 3. 26. 22:31
 React Hook useEffect has a missing dependency: '~~~'. Either include it or remove the dependency array

이 경고는 다들 한 번씩, 아니 매일 볼 수 있는 경고다.

 

이 경고가 뜨는 경우는 아마 다들 이럴 것이다

"렌더링 되고 딱 한 번만 실행하게 하자!"

"이 데이터가 변할 때만 실행되게 하자!"

 

보통은 useEffect를 사용하고 의존성 배열에 빈 배열을 넣는다.

근데 항상 그 안에서 실행하는 함수나 참조하는 데이터 때문에 의존성 배열에 경고가 뜬다.

"이거 변할 수 있는데 얼른 의존성 배열에 넣어줘"

 

하지만 경고라서 무시하고 넘어가면 내가 원하는 대로 작동하긴 할 것이다.

 

그렇지만 이렇게  한다면

의존성 누락으로 인해 만약 useEffect내부의 값이 바뀌고 재랜더링이 되어야 한다면 useEffect는 이러한 변경을 감지 못하고 버그로 이어질 수 있다.

그니까 만약 의존되는 값들이 변하는데 useEffect는 재실행이 안되니까 예상치 못한 동작을 할 수 있다.

 

그리고 코드의 명시성이 감소한다. 

코드를 읽는 다른 개발자들이 해당 코드의 의도를 정확히 파악하기 어려울 수 있다.

이는 코드의 유지보수성을 저하시키는 원인이 될 수 있다.

 

그래서 보통은 아래와 같이 해결할 것이다.

1. 경고의 말을 듣고, 데이터 또는 함수 의존성 배열에 넣어주기

2. 해당 함수, 변수를 useEffect 훅 내에서 정의하기

3. 주석을 통해 경고 무시하기

 

근데 3번은 지양해야한다.

이는 임시로 해결하는 것이기 때문이다.

 

그래서 2번을 보통 많이 하는데 만약 useEffect내에 넣지 못하는 상황이라면 어떨까?

 

팀프로젝트를 하면서 팀원이 겪은 상황이다.

커스텀 훅을 통해서 반환되는 함수를 useEffect 내에서 호출하게 했다.

그러면 당연하게도 종속성 배열에 해당 함수를 넣어달라고 한다.

 

실제 코드를 보자

 const { topComponent, topFunnelPage, pushFunnel } = useFunnel([
    <UserProfileSetting
      setNickName={setNickName}
      key="1"
    />,
    <SelectResidence
      setAddress={setAddress}
      key="2"
    />,
    <SelectCategory key="3" />,
    <FinishFunnel
      profileImage={profileImage}
      category={category}
      nickName={nickName}
      address={address}
      id={id}
      passWord={passWord}
      key="4"
    />
  ]);
  
  
  useEffect(() => {
    if (address.dong) {
      pushFunnel();
    }
  }, [address]);

많이 생략했지만 의도를 말하자면

useFunnel이라는 함수는 3개의 컴포넌트를 받아서
pushFunnel을 할 때마다 3개의 컴포넌트를 순회한다.

즉 1번 컴포넌트에서 pushFunnel을 하면 2번 컴포넌트로 교체하는 것이다.

 

근데 아래와 같이 경고가 뜬다.

위에서 말했듯이 당연하다.

pushFunnel은 변할 수 있는 녀석인데 의성에 안 넣어주니까 '넣어주세요'라고 한다.

사실 안 넣어도 원하는 대로 작동한다.

하지만 위에서 말했듯이 좋지 않은 방식이다.

 

그럼 어떻게 해결을 할까?

 

 

1. pushFunnel을 useCallback으로 감싸주기

  const dataFn = useCallback(() => pushFunnel(), [pushFunnel]);

  useEffect(() => {
    if (address.dong) {
      dataFn();
    }
  }, [address, dataFn]);

useCallback으로 pushFunnel을 다른 변수에 저장하고 pushFunnel이 바뀌지 않음을 명시해 주었다.

pushFunnel을 useCallback의 의존성 배열에 넣긴 했지만 우리는 pushFunnel이 바뀌지 않는 함수인 것을 알지 않는가?

 

 

여기서 잠깐!!

함수가 왜 바뀌는 거야?

리액트의 함수 컴포넌트는 자바스크립트 함수고, 이 함수가 호출될 때마다 함수 내부의 변수와 함수들은 새로운 실행 콘텍스트에서 실행된다. 이 때문에 컴포넌트가 다시 렌더링 될 때마다 내부에서 선언한 함수들도 새로운 메모리 주소를 가진 새로운 함수로 생성된다.

함수가 새로운 메모리 주소를 가지는 이유가 뭐야?

함수가 새로운 메모리 주소를 가지게 되는 이유는 자바스크립트와 같은 프로그래밍 언어에서 함수가 일급 객체(first-class object)로 취급되기 때문이다.

이 말은 함수가 변수에 할당될 수 있고, 다른 함수의 인자로 전달될 수 있으며, 함수에서 또 다른 함수를 반환할 수 있다는 의미다.

이러한 특성 때문에 함수는 실행될 때마다 새로운 실행 콘텍스트(execution context)를 생성하고,

이에 따라 새로운 메모리 공간을 할당받게 됩니다.

함수 내부에서 선언된 변수나 함수는 그 함수가 실행될 때 생성되고, 함수 실행이 완료되면 그 변수와 함수는 가비지 컬렉션의 대상이 된다.

그러나 함수가 다시 호출되면, 이전의 변수나 함수들과는 전혀 다른 새로운 메모리 공간에 다시 생성된다.

 

이러한 동작 방식은 프로그래밍 언어의 스코프실행 콘텍스트의 개념과 밀접하게 연관되어 있습니다.

각 함수 호출은 자신만의 스코프를 가지며, 이 스코프 내에서 선언된 변수와 함수는 해당 호출에서만 유효합니다.

따라서 같은 함수 코드를 가지고 있더라도, 각 함수 호출은 서로 다른 메모리 공간을 차지하게 된다.

 

 

아무튼 다시 돌아와서 

위 코드를 작성하면 엄청난 오류를 뱉어낸다.

무한 반복을 하게 된다.

 

왜 그런 것일까?

당연한 것이지만 차근차근 설명해 보겠다.

 useFunnel 내부를 보자

export default function useFunnel(funnelList: ReactNode[]) {
  const [funnelState, setFunnelState] = useState<FunnelStateType>({
    nowFunnelNum: 0,
    topComponent: funnelList[0]
  });

...
 

  const pushFunnel = () => {
    setFunnelState(({ nowFunnelNum }) => {
      return {
        nowFunnelNum: nowFunnelNum + 1,
        topComponent: funnelList[nowFunnelNum + 1]
      };
    });
  };
  
  ...

}

pushFunnel은 함수다

그니까 렌더링 되면 메모리주소가 바뀐다.

렌더링이 될 때마다 바뀌니까 pushFunnel이 실행되었을 때 set함수가 실행될 것이고 렌더링이 된다.

그럼 또 pushFunnel의 주소가 바뀌고 또 set함수가 실행되고... 무한반복이다.

 

그러면 이것도 useCallBack으로 감싸줘야 한다.

 const pushFunnel = useCallback(() => {
    setFunnelState((prev) => {
      return {
        nowFunnelNum: prev.nowFunnelNum + 1,
        topComponent: funnelList[prev.nowFunnelNum + 1]
      };
    });
  }, [funnelList]);

이제 될까?

 

아니다. 똑같은 에러를 뱉어낸다.

왜 그럴까?

 

사실 또 당연하지만 코드를 자세히 봐보자

export default function useFunnel(funnelList: ReactNode[]) {
  const [funnelState, setFunnelState] = useState<FunnelStateType>({
    nowFunnelNum: 0,
    topComponent: funnelList[0]
  });

funnelList가 배열이다.

배열도 똑같다. 렌더링 될 때마다 참조되는 메모리 주소 값이 변경된다. 배열도 일급객체다.

 

그럼 이것도 useMemo를 이용해서 묶어주자.

export default function useFunnel(funnelList: ReactNode[]) {
  const FunnelListMemo = useMemo(() => funnelList, [funnelList]);
  const [funnelState, setFunnelState] = useState<FunnelStateType>({
    nowFunnelNum: 0,
    topComponent: FunnelListMemo[0]
  });

이제는 아는 사람도 있을 것이다.

당연히 또 에러를 뱉어낸다.

 

왜인지도 당연히 알 것이다. 
props로 받아오는 funnelList도 배열이다.

그럼 다시 돌아가서 useFunnel에 인수로 넣어준 배열을 보자

const { topComponent, topFunnelPage, pushFunnel } = useFunnel([
      <UserProfileSetting
        setNickName={setNickName}
        key="1"
      />,
      <SelectResidence
        setAddress={setAddress}
        key="2"
      />,
      <SelectCategory key="3" />,
      <FinishFunnel
        profileImage={profileImage}
        category={category}
        nickName={nickName}
        address={address}
        id={id}
        passWord={passWord}
        key="4"
      />
  ]);

이 부분 또한 바꿔줘야 한다.

그러면 아래와 같이 된다.

 const funnelList = useMemo(
    () => [
      <UserProfileSetting
        setNickName={setNickName}
        key="1"
      />,
      <SelectResidence
        setAddress={setAddress}
        key="2"
      />,
      <SelectCategory key="3" />,
      <FinishFunnel
        profileImage={profileImage}
        category={category}
        nickName={nickName}
        address={address}
        id={id}
        passWord={passWord}
        key="4"
      />
    ],
    [
      setNickName,
      setAddress,
      profileImage,
      category,
      nickName,
      address,
      id,
      passWord
    ]
  );

  const { topComponent, topFunnelPage, pushFunnel } = useFunnel(funnelList);

이렇게 코드를 만들면 이제 오류를 뱉어내지 않을 것이다.

 

조금 이상할 수 또 있다.

이렇게까지 많은 메모이제이션을 해야 할까?

애초에 설계부터 바꿔서 해결할 수도 있다.

그렇지만 만약 그렇지 못한 상황에서는 차근차근 위에서부터 의존되는 데이터들을 확인해야 한다.

 

2. useState에서 set함수의 함수형 업데이트를 사용하자

아래 코드를 보자

 const [isVisible, setIsVisible] = useState(false);
  const [lastScrollTop, setLastScrollTop] = useState(0);
  
useEffect(() => {
    const handleScroll = () => {
      const currentScrollTop =
        window.scrollY || document.documentElement.scrollTop;
      if (currentScrollTop > lastScrollTop) {
        // 아래로 스크롤했을 때
        setIsVisible(true);
      } else {
        // 위로 스크롤했을 때
        setIsVisible(false);
      }
      setLastScrollTop(currentScrollTop <= 0 ? 0 : currentScrollTop);
    };
    
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

 

스크롤을 감지하고 만약 아래로 스크롤했다면 요소를 보이게 하고,

위로 스크롤 했을 때 요소를 안 보이게 하는 로직이다.

보통 내비게이션 바가 안 보이도록 할 때 사용한다.

 

근데 위 로직에서도 경고가 뜬다.

React Hook useEffect has a missing dependency: 'lastScrollTop'. 
Either include it or remove the dependency array. (eslintreact-hooks/exhaustive-deps)

그래서 아래와 같이 넣어줬다.

 const [isVisible, setIsVisible] = useState(false);
  const [lastScrollTop, setLastScrollTop] = useState(0);
  
useEffect(() => {
    const handleScroll = () => {
      const currentScrollTop =
        window.scrollY || document.documentElement.scrollTop;
      if (currentScrollTop > lastScrollTop) {
        // 아래로 스크롤했을 때
        setIsVisible(true);
      } else {
        // 위로 스크롤했을 때
        setIsVisible(false);
      }
      setLastScrollTop(currentScrollTop <= 0 ? 0 : currentScrollTop);
    };
    
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [lastScrollTop]);

근데 이렇게 하면 lastScrollTop이 바뀔 때마다 불필요하게 handleScroll을 만들고 이벤트를 등록한다.

 

그래서 방법을 찾던 중 아래와 같은 방법을 사용할 수 있었다.

 const [isVisible, setIsVisible] = useState(false);
  const [lastScrollTop, setLastScrollTop] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      const currentScrollTop =
        window.scrollY || document.documentElement.scrollTop;
      setLastScrollTop((prevScrollTop) => { //set함수에서 업데이트 함수를 사용
        if (currentScrollTop > prevScrollTop) {
          setIsVisible(true);
        } else {
          setIsVisible(false);
        }
        return currentScrollTop <= 0 ? 0 : currentScrollTop;
      });

    };
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

이 방법은 함수형 업데이트라고 하며, 이를 사용함으로써 최신 상태를 참조할 수 있게 된다.

이 경우, setCount 내부에서 사용되는 prevScrollTop은 현재의 prevScrollTop 상태 값을 참조하는 것이 아니라, 업데이트를 실행할 때의 상태 값을 참조한다.

따라서, useEffect의존성 배열에 lastScrollTop을 추가하지 않아도 된다.

왜냐하면 setCount에 전달된 함수 내부에서 참조하는 상태는 useEffect가 실행될 때마다 자동으로 최신 상태를 반영하기 때문이다.

 

 

3.Basic comparison

찾아보니 아래와 같은 방법도 존재했다.

렌더링이 원하는 대로 않는 등 필요한 경우에는 useEffect를 빠르게 끝내 해결하는 방법이다.

useEffect(() => {
  if (post.id === undefined) {
    return;
  }
  arrFn(post);
}, [post]);

 

 

4. useEffcet 내부에서 함수 선언하기?

아래와 같이 해결하기도 한다.

useEffect(() => {
  const arrFn = (count) => {
    // do something
  };
  arrFn(count);
}, [count])

근데 이 방법은 함수가 많아지면 많아질수록 가시성이 떨어지고, useEffect 내부에서 어떻게 동작하는지 파악하기가 어려워진다고 한다.

그래서 보통 아래와 같이 useCallback을 사용한다고 한다.

const arrFn = useCallback((count) => {
  // do something
}, []);

useEffect(() => {
  arrFn(count);
}, [count, arrFn]);

 

728x90
반응형
LIST