Original article: React.useEffect Hook – Common Problems and How to Fix Them

감수 Boyeon Ihn

React 훅(hook)은 꽤 오랫동안 사용되어 왔습니다. 개발자 대부분이 이 훅의 동작 원리와 일반적인 사용 사례에 대해 제법 익숙합니다. 하지만 많은 이들이 빠지는 useEffect 함정이 있습니다.

사용 사례

간단한 시나리오로 이야기를 시작해 보겠습니다. React App을 만들고 한 컴포넌트에 현재 사용자의 이름을 보여주기를 원한다고 하겠습니다. 우선, API로부터 사용자 이름을 불러와야 합니다.

또한, 애플리케이션 내 다른 곳에서도 사용자 데이터가 사용될 것이기 때문에 데이터 불러오는 로직(logic)을 커스텀(custom) React 훅에 담아보겠습니다.

기본적으로 이 React 컴포넌트는 다음과 같을 것입니다.

const Component = () => {
  // useUser custom hook

  return <div>{user.name}</div>;
};

아주 간단해 보이지 않나요?

useUser React 훅

두번째 단계는 useUser 커스텀 훅을 만드는 것입니다.

const useUser = (user) => {
  const [userData, setUserData] = useState();
  useEffect(() => {
    if (user) {
      fetch('users.json').then((response) =>
        response.json().then((users) => {
          return setUserData(users.find((item) => item.id === user.id));
        })
      );
    }
  }, []);

  return userData;
};

하나씩 자세히 살펴보겠습니다. 우선 이 훅이 사용자 객체를 받는지 확인을 합니다. 그런 다음에 user.json이라는 파일로부터 애플리케이션 사용자의 목록을 불러오고 필요한 사용자를 찾기 위해 아이디(id)로 목록을 걸러냅니다.

그런 다음에 필요한 데이터를 갖게 되면, 그 데이터를 훅에 있는 userData state에 저장합니다. 마지막으로 userData를 반환합니다.

주의: 위 예시는 단지 설명을 위한 것입니다. 실제로 데이터 불러오기는 이보다 훨씬 복잡합니다. 이 주제에 관해 관심이 있다면 ReactQuery, Typescript and GraphQL으로 데이터 불러오기 설정을 어떻게 하는지에 관한 제 글을 확인하시길 바랍니다.

이제 훅을 React 컴포넌트에 적용하면 어떻게 되는지 확인해보겠습니다.

const Component = () => {
  const user = useUser({ id: 1 });
  return <div>{user?.name}</div>;
};

모든 게 예상대로 작동하는 것처럼 보입니다. 잠깐만... 이게 뭐죠?

ESLint exhaustive-deps 규칙

훅(hook)에 ESLint 경고가 생겼습니다.

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

음, useEffect에 디펜던시(dependency)가 빠진 것으로 보입니다. 자, 그럼! 디펜던시를 추가해보겠습니다. 별 대수겠어요? 😂(여기서 디펜던시는 한 대상과 의존관계에 있는 다른 대상을 일컫습니다. 즉, useEffect 훅이 디펜던시에 영향을 받는다는 것으로 이해하시면 되겠습니다. - 역주)

const useUser = (user) => {
  const [userData, setUserData] = useState();
  useEffect(() => {
    if (user) {
      fetch('users.json').then((response) =>
        response.json().then((users) => {
          return setUserData(users.find((item) => item.id === user.id));
        })
      );
    }
  }, [user]);

  return userData;
};

이런, 이제는 Component가 렌더링(rendering)을 멈추는 것 같지 않아 보입니다. 어떻게 된 일일까요? - (렌더링은 간단하게 화면에 보이는 요소들을 불러오는 것으로 이해하시면 되겠습니다. - 역주)
설명해보겠습니다.

무한 렌더링(re-render) 문제

컴포넌트가 무한 렌더링(re-rendering)을 하는 이유는 useEffect의 디펜던시가 끊임없이 변하기 때문입니다. 하지만 우리는 계속 같은 객체를 훅에 전달하고 있는데 왜 문제가 발생할까요?
같은 key와 value를 가진 객체를 전달하고 있는 것은 맞지만 엄밀히 말하면 같은 객체는 아닙니다. 사실 Component가 렌더링할 때마다 새로운 객체가 생성됩니다. 그런 다음 이 새로 생긴 객체를 useUser 훅의 인자(argument)로 전달됩니다.
useEffect가 두 객체를 비교할 때 서로 다른 참조 값(reference) 가지고 있기 때문에 다시 사용자들을 불러오고 새로운 사용자 객체를 state에 저장합니다. 그리고 그 state의 업데이트에 의해 컴포넌트의 리렌더링을 일으키는 것입니다. 무한 반복이 되는 거죠. (Reference는 한 변수를 선언할 때 해당 변수에 생기는 참조 값입니다. 그 변수의 별명 같은 것으로 보면 됩니다. 같아 보이는 두 객체라도 각기 다른 변수에 저장이 되면 각각의 참조 값은 다른 값을 가집니다. - 역주)
그러면 어떻게 해결할 수 있을까요?

해결 방법

이제 문제를 파악했으니 해결책을 찾을 수 있습니다.
첫번째, 그리고 아마 가장 확실한 방법은 useEffect 디펜던시 배열에서 해당 디펜던시를 제거하고 ESLint 규칙을 무시하고 그냥 넘어가는 것입니다.
하지만 이 방법은 잘못된 접근입니다. 이 방법은 앱에 에러와 예기치 못한 동작을 초래할 수 있습니다(그리고 분명 그럴 것입니다). useEffect가 어떻게 동작하는지 더 알고 싶은 분께 Dan Abramov`s 완벽 가이드를 강력하게 추천합니다.

그러면 다음 해결책은 뭘까요?

이 예시 같은 경우에 가장 쉬운 해결책은 { id: 1 } 객체를 컴포넌트에서 빼내는 것입니다. 이 방법은 그 객체에 동일한 참조 값을 주고 문제를 해결해 줄 것입니다.

const userObject = { id: 1 };
const Component = () => {
  const user = useUser(userObject);
  return <div>{user?.name}</div>;
};
export default Component;

하지만 이 방법은 언제나 가능한 것은 아닙니다. 사용자 id가 컴포넌트 props 혹은 state에 의존한다고 가정해보겠습니다.

예를 들어, 사용자 id에 접근하기 위해 URL 파라미터를 사용하는 상황 같은 것일 수 있습니다. 만약 이러한 경우라면 그 객체를 기억하고 동일한 참조 값을 보장해 줄 useMemo를 이용하면 됩니다.

const Component = () => {
  const { userId } = useParams();
  const userObject = useMemo(() => {
    return { id: userId };
  }, [userId]); // Don't forget the dependencies here either!
  const user = useUser(userObject);
  return <div>{user?.name}</div>;
};
export default Component;

마지막으로 useUser 훅에 객체 변수를 보내는 대신에 원시 값(primitive value)인 사용자 id만 보내는 게 가능합니다. 이 방법은 useEffect 훅에서 참조 값 동일 문제를 예방할 수 있습니다.

const useUser = (userId) => {
  const [userData, setUserData] = useState();
  useEffect(() => {
    fetch('users.json').then((response) =>
      response.json().then((users) => {
        return setUserData(users.find((item) => item.id === userId));
      })
    );
  }, [userId]);
  return userData;
};
const Component = () => {
  const user = useUser(1);
  return <div>{user?.name}</div>;
};

해결됐네요!

그리고 이 과정을 통해서 ESLint 규칙을 깨뜨릴 필요도 없었네요.

주의: 커스텀 훅에 보낸 인자가 객체가 아니라 함수라면 무한 리-렌더링을 피하기 위해 비슷한 기술을 사용했을 것입니다. 주요한 차이는 위의 예시에서 사용한 useMemo 대신 useCallback을 사용해야 합니다.

읽어 주셔서 감사합니다!

코딩에 대해 궁금하시나요? 여기서 맘껏 코딩해보세요.

Twitter와 개인 블로그에 더 많은 React 관련 컨텐츠가 있으니 팔로우하러 놀러오세요.

이미지 출처 vectorjuice