useEffect 무한렌더링 .. 근데 useCallback을 곁들인

프로젝트 중 useEffectfetch를 써야하는 경우가 많은데, 무한 렌더링이 발생하는 경우가 생겼다. 무한 렌더링이 발생하는 원인을 찾아보니 useEffect()의 흐름을 더 생각해보는 계기가 되었고 좀 더 최적화하는 방법으로 useCallback()을 알게되었다.

아래 코드는 useLocation() hooks 를 이용해 현재 경로 정보를 담고 있는 객체의 search 값을 받아 해당하는 데이터를 fetch 해오는 코드이다.

const [category, setCategory] = useState([]);

useEffect(() => {
    async function fetchAndSetCategory() {
      const response = await fetch(`${API.MenuList}${location.search}`);
      const data = await response.json();
      setCategory(data.results);
    }
    fetchAndSetCategory();
	 }, [category]);         *// 수정 전 코드* 
	 }, [location.search]);     *// 1차 수정 후 코드* 

초기의 로직은 카테고리 state 가 변경될 때마다 url을 변경해야 한다는 로직을 짰었다. useEffect의 dependency 에 category state를 직접 넣어줬었으며 그 결과로, useEffect 무한루프에 빠지게 되어서 백엔드에게 무한 fetch를 시켜버리는 상황이 발생하였다.

그 이유는 자바스크립트에선 함수가 객체로 취급되기 때문에 동일한 코드의 함수라도 메모리 주소에 의한 참조 비교가 일어나기 때문에, 두 함수를 엄격한 동등 연산자(===) 사용하여 비교 시, false 값이 발생하게 된다.
하여서 아래 코드의 API를 호출하는 함수인 fetchAndSetCategory()${location.search}가 바뀌든 말든 컴포넌트가 렌더링 될 때마다 새로운 참조 값으로 변경되고 state 변경으로 인해 또 useEffect가 실행되버리고 그럼 새로 또 렌더링되는 악순환을 반복하게 된 것이다.

그래서 1차적으로, dependency[location.search]를 넣어줘 url이 변경될 때 useEffect를 실행시키는 것으로 무한루프에서 벗어날 수 있었다. 하지만 이것또한 렌더링할 때마다 useEffect 내의 함수를 재호출하게 되어서 불필요한 렌더링을 발생시킨다.

2차적으로, 이것보다 좀 더 최적화된 코드로 아래와 같이 useCallback hooks 사용하였다. useCallback()은 의존성이 변경되는 경우, 이전에 기억하고 있던 함수 자체와 비교해서 다른 경우에만 리랜더시킨다. 컴포넌트가 다시 랜더링되더라도 fetchAndSetCategory() 함수의 참조값을 동일하게 유지시킬 수 있다. 따라서 의도했던 대로, useEffect()에 넘어온 함수는 location.search 값이 변경되지 않는 한 재호출 되지 않게 된다.

useCallback() 이용한 2차 수정 코드

const fetchData = useCallback(() => {
    async function fetchAndSetCategory() {
      const response = await fetch(`${API.MenuList}${location.search}`);
      const data = await response.json();
      setCategory(data.results);
    }
    fetchAndSetCategory();
  }, [location.search]);

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

좋은 웹페이지 즐겨찾기