React Hooks 프로젝트 전환 후기 - useEffect 의존성 배열 삽질기
문제 상황
사내 대시보드 프로젝트를 Hooks로 전환하던 중, 데이터 페칭 로직에서 무한 루프가 발생했다. Class 컴포넌트에서는 멀쩡히 동작하던 코드였다.
// 기존 Class 컴포넌트
componentDidMount() {
this.fetchData(this.props.userId);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchData(this.props.userId);
}
}
이걸 Hooks로 변환하면서 이렇게 작성했다.
const [data, setData] = useState([]);
useEffect(() => {
fetchData(userId).then(res => setData(res));
}, [data]); // 잘못된 의존성
당연히 무한 루프가 발생했다. setData가 호출되면 data가 변경되고, 다시 useEffect가 실행되는 구조였다.
해결 과정
의존성 배열에는 effect 내부에서 사용하는 props나 state를 넣어야 한다는 원칙을 다시 확인했다. data는 effect의 결과물이지 입력값이 아니었다.
useEffect(() => {
fetchData(userId).then(res => setData(res));
}, [userId]); // 올바른 의존성
또 다른 케이스로, 객체를 의존성으로 넣었을 때도 문제가 있었다.
const filters = { category, keyword };
useEffect(() => {
fetchData(filters);
}, [filters]); // 매번 새로운 객체 참조
매 렌더링마다 새로운 객체가 생성되어 불필요한 API 호출이 발생했다. useMemo로 해결했다.
const filters = useMemo(
() => ({ category, keyword }),
[category, keyword]
);
배운 점
useEffect는 componentDidMount + componentDidUpdate를 합친 것이 아니다. 렌더링 후 실행되는 side effect 함수다.- 의존성 배열은 "언제 실행할지"가 아니라 "어떤 값에 의존하는지"를 명시한다.
- ESLint의
exhaustive-deps규칙을 켜두고 경고를 무시하지 않는 게 중요하다.
Hooks의 멘탈 모델을 제대로 이해하는 데 시간이 좀 걸렸지만, 이제는 Class 컴포넌트로 돌아가고 싶지 않을 만큼 편하다.