트러블 슈팅: useEffect 무한루프
트러블 슈팅(Trouble Shooting)
리팩토링 과정에서 의도하지 않은 동작이나 오류를 마주했다. 이번 글은 이를 해결한 과정에서 배운 점을 정리한 것이다.
문제 인식
useEffect의 무한루프
목적지 리스트가 렌더링 되지 않는 원인을 파악하고자 console.log로 코드를 점검하던 중, useEffect의 무한 루프를 발견했다. 코드상으로, 목적지 리스트 fetch 함수는 useEffect가 실행하고 있었다. 이에 따라 useEffect 무한 루프를 먼저 해결하기로 했다.
가설 설정 및 검증
가설 1. useEffect 의존성 배열이 잘못 작성되었다.
가설 검증하기:
useEffect의 의존성 배열 점검
- 첫번째로
useEffect의 의존성 배열을 점검했다.useEffect가 참조하고 있던 의존성을 제거하고 다시 넣어보며 코드를 재실행했다. 그 결과,useEffect의 의존성 배열에 하나라도 참조값이 존재하면useEffect가 무한히 반복된다는 사실을 파악했다. 다음은 문제가 발생한useEffect코드이다.
1
2
3
4
5
6
7
8
9
10
useEffect(() => {
getCategoryIdList();
getfilteredResult(searchQueryParams, defaultCategoryIdList);
console.log("debug useDestinationsFetch useEffect"); //디버깅을 위해 추가한 line
}, [
getCategoryIdList,
getfilteredResult,
searchQueryParams,
defaultCategoryIdList,
]);
useEffect는 흔히, 내부 로직이useEffect가 참조하고 있는 값을 변경하면서 무한 반복된다. 그러나 문제가 발생한useEffect에는 참조값을 바꾸는 로직이 없었다.useEffect가 참조하는 함수들도useCallback으로 메모리 주소값을 고정해두었기 때문에 렌더링 마다 다른 함수로 인식할 가능성이 없었다.
코드상으로는 로직에 문제가 없어 보였지만, 확실히 하고자 의존성 배열의 값들을 하나씩 뺐다 넣어보며 점검했다.
그러나 로직에는 문제가 없다는 내 생각과 달리,
defaultCategoryIdList을 의존성 배열에서 제거하자 무한 루프가 되지 않았다. 즉,getfilteredResult함수의 인자로 사용되는defaultCategoryIdList가 무한 루프의 원인이었던 것이다.defaultCategoryIdList와 관련된 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
const [defaultCategoryIdList, setDefaultCategoryIdList] = useState<number[]>([]);
const getCategoryIdList = useCallback(async () => {
const res = await getAllCategoryList();
setDefaultCategoryIdList(extractCategoryIdFromCategoryList([res?.data]));
return;
}, [
getAllCategoryList,
setDefaultCategoryIdList,
extractCategoryIdFromCategoryList,
]);
문제 해결하기:
useEffect코드 분석하기
getCategoryIdList함수는DefaultCategoryIdList의 상태를 변경하고 있다. 이에 따라getCategoryIdList함수가 호출되면DefaultCategoryIdList를 참조하는 사이드 이펙트가 실행된다.
이 때, getCategoryIdList를 호출하는 사이드 이펙트가 발생하면서 위 과정이 반복된다.
즉, DefaultCategoryIdList 변경 -> 사이드 이펙트 발생이 끝없이 반복되면서 useDestinations 커스텀 훅이 무한루프 된다.
이를 해결하기 위해
useEffect의 코드를 분석했다.useEffect는 다음의 두 가지 기능을 수행한다.getCategoryIdList함수 호출getfilteredResult함수 호출
위의 두 함수를 연이어 호출한 이유는
getCategoryIdList를 실행했을 때 얻을 수 있는categoryIdList가getfilteredResult의 인자로 필요했기 때문이다.다시 말해,
getCategoryIdList가 하나의 독립적인 함수임을 인지하지 못하고 단순히categoryIdList를 얻기 위한 매개체로서의 역할만 생각했다.하나의
useEffect가 두 가지 기능을 수행하도록 만들었다는 사실을 파악하지 못하고 ‘getCategoryIdList가 실행되도록 만들었다!’ 정도로만 생각하고 넘어간 것이다.
getCategoryIdList의 로직을useEffect내부에서 파악할 수 없는 점도 문제를 빠르게 인지하지 못한 원인이기도 하다.getCategoryIdList내부의 상태변경 함수를useEffect코드를 살피는 것 만으로 알 수 없기 때문에 무엇이 잘못되었는지 인지하지 못했다.
문제 해결하기:
useEffect내부 코드 분리하기
- 위와 같은 인사이트를 토대로, 두 가지 기능을 수행하는
useEffect의 코드를 분리하기로 결정했다. 1차로 변경한 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
//useEffect 로직
useEffect(() => {
getCategoryIdList();
}, [getCategoryIdList]);
useEffect(() => {
getfilteredResult(searchQueryParams, defaultCategoryIdList);
console.log("debug useDestinationsFetch useEffect"); //디버깅을 위해 추가한 line
}, [getfilteredResult, searchQueryParams, defaultCategoryIdList]);
한편, useEffect의 코드를 분리하면서 getCategoryIdList의 이질적인 면도 파악할 수 있게 되었다. ‘getCategoryIdList함수의 역할이 현재 이 코드가 위치한 useDestinations 커스텀 훅의 목적과 결이 맞는가?’ 하는 의문이 들었기 때문이다.
useDestinations 커스텀 훅은 여행지 리스트 데이터를 fetch하기 위해 만든 커스텀 훅이다. 게다가 카테고리 데이터를 fetch하는 로직을 담당하는 useCategory 커스텀 훅도 이미 존재하고 있다.
useEffect의 코드를 분리한 결과, getCategoryIdList가 useDestinations 커스텀 훅 자체와 관련이 낮다는 점을 깨달았다.
이러한 인사이트를 얻어, getCategoryIdList 함수는 useCategory 커스텀 훅으로 이동하게 되었다. 그리고 getfilteredResult함수는 useCategory에서 관리하는 state를 인자로 받도록 코드를 수정하였다.
1
2
3
4
5
const { categoryList, categoryIdList, selectedCategory } = useCategory();
useEffect(() => {
getfilteredResult(searchQueryParams, selectedCategory);
}, [getfilteredResult, searchQueryParams, selectedCategory]);
고찰
이번 트러블 슈팅으로 배운 점이 두 가지 있다.
첫째, 하나의 함수는 하나의 역할만 하도록 코드를 작성하자.
두 가지 기능을 수행하는 사이드 이펙트는 무한루프에 빠질 수 있다.
둘째, 코드를 선언적으로 작성하자.
내 코드가 정상적으로 보여도 컴퓨터는 거짓말하지 않는다. 무한루프가 발생하면 useEffect의 영향범위를 다시 한 번 점검하자. 무엇보다도, 코드를 선언적으로 작성하여 이러한 휴먼에러에 기인한 오류를 최소화 하도록 하자.
