[React] useEffect 무한루프 탈출하는 방법

이은진·2021년 1월 3일
10

React Study

목록 보기
2/10
post-thumbnail

기존에 클래스형 컴포넌트 방식으로 짠 코드를 함수형 컴포넌트로 변환을 했는데, 특정 컴포넌트만 클릭하면 미친듯이 렌더가 일어나 브라우저가 다운되는 현상을 겪었다. 알고 보니 componentDidMount를 대체하는 함수인 useEffect 에서 무한루프가 일어난 것이었다.

1. 문제상황

만들고 있는 에어비앤비 사이트의 숙소 리스트에서, 숙소 유형 탭을 클릭하면 네 개의 선택지가 나오고, 원하는 형태의 숙소를 클릭하고 저장을 누르면 선택한 항목으로 필터된 결과로 다시 렌더된다. 그런데 문제는 [숙소 유형] 탭을 클릭할 때도, 그 안의 항목들을 체크할 때도 무한루프에 빠졌다. 리액트 Hook의 함수들에 익숙치도 않은 나는 고민에 빠졌다.

원리도 잘 이해하지 못한 채 기계적으로 클래스형에서 함수형으로 바꾼 터라 들여다봐도 문제가 해결될 리 없었다. 그런데 분명 가만히 있을 때는 아무 문제 없는데, 메뉴를 클릭할 때부터 렌더가 몇백 번이고 일어난다는 것이 이상했다. 그래서 드롭다운 메뉴를 여는 useEffect 함수와, 또 다른 useEffect 함수를 사용하여 fetch를 하는 부분을 들여다보았다. 다음은 기존에 내가 짰던 코드다.

  const [roomTypeFilters, setRoomTypeFilters] = useState([]);
  const [roomTypeFilteredResult, setRoomTypeFilteredResult] = useState([]);

  useEffect(() => {
    fetch("/data/RoomList/roomTypeFilters.json")
      .then((res) => res.json())
      .then((data) => {
        const newData = data.roomTypeFilters.map((filter) => {
          return { ...filter, isChecked: false };
        });
        setRoomTypeFilters(newData);
      });
  });

  useEffect(() => {
    document.body.style.overflow = "hidden";
    backgroundRef.current.addEventListener("mousedown", handleClickOutside);
  });

다방면으로 알아보니, 문제는 useEffect 함수 안에서 setState를 함으로써 발생되는 것이었다. useEffect는 componentDidMount 함수와 비슷한 역할을 한다. 렌더가 일어나면 componentDidMount 함수 안의 코드가 실행이 된다. 그런데 문제는 다른 코드로 인해서 다른 용도로 렌더가 일어났는데 불필요한 렌더가 일어날 때다. 이 경우 브라우저가 현저히 느려지고 리액트의 성능이 안 좋아지는 결과를 초래한다.

2. 해결 방법

리액트 Hook에서 componentDidUpdate의 역할을 하는 것은 두 번째 인자다. useEffect 함수는 두 개의 인자를 가지는데, 첫 번째는 그 함수가 어떤 작동을 할지에 대한 arrow function이며, 두 번째는 한 개의 배열이다. 이 배열이 내가 겪었던 문제를 푸는 key가 된다.

  const [roomTypeFilters, setRoomTypeFilters] = useState([]);
  const [roomTypeFilteredResult, setRoomTypeFilteredResult] = useState([]);

  useEffect(() => {
    fetch("/data/RoomList/roomTypeFilters.json")
      .then((res) => res.json())
      .then((data) => {
        const newData = data.roomTypeFilters.map((filter) => {
          return { ...filter, isChecked: false };
        });
        setRoomTypeFilters(newData);
      });
  }, []);

  useEffect(() => {
    document.body.style.overflow = "hidden";
    backgroundRef.current.addEventListener("mousedown", handleClickOutside);
  }, []);

단순히 useEffect의 두 번째 인자로 빈 배열을 넣어주었더니 무한루프는 거짓말처럼 사라졌다. 이 두 번째 인자는 "의존성 배열"이라고 한다. 첫 번째 인자로 등장하는 함수가, 두 번째 인자로 등장하는 배열의 원소가 무엇인지에 따라 의존적으로 이벤트를 실행한다는 의미다. 내 경우 이벤트가 내가 직접적으로 메뉴를 오픈하거나, 필터 항목에 체크를 할 때만 함수를 실행시키라는 의미로 빈 배열을 넣어주었다. 이 배열에 어떤 인자가 들어간다면, 그 인자에 변화가 생겼을 때 useEffect 함수가 실행이 된다.

리액트의 라이프사이클을 명확하게 이해하고 있어야 한다는 것을 알게 되었다. 아무리 라이브러리 및 언어의 성능이 좋아져서 최적화를 알아서 한다고는 해도, 항상 unmount 및 update의 시점을 어떻게 두어야 할지 염두에 두고 있어야 한다는 것을 알게 되었다. 마찬가지의 맥락에서 useMemo와 useCallback 또한 성능 최적화를 위해 두 번째 인자로 오는 배열의 원소가 무엇인지에 따라서 컴포넌트의 재렌더를 판단하는 함수다. 지금은 useEffect를 위주로 쓰고 있지만, 점차 코드 리팩토링을 통해 다양한 "use 어쩌고" 함수를 활용해보려 한다.

profile
빵굽는 프론트엔드 개발자

0개의 댓글