🔍 검색 키워드: React useEffect 무한루프 해결, useEffect 의존성 배열 오류, React 렌더링 무한 반복, useEffect deps 객체 함수 참조, React hooks 무한루프 원인
React useEffect 무한루프 완벽 해결 가이드 — 의존성 배열 원인과 해결방법
증상: 브라우저가 멈추거나 콘솔이 폭발한다
React 앱을 개발하다 보면 어느 순간 브라우저 탭이 멈추거나, 콘솔에 같은 로그가 수천 번 찍히거나, "Maximum update depth exceeded" 에러를 마주치게 된다.
Warning: Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a dependency array,
or one of the dependencies changes on every render.
혹은 Network 탭에서 API 요청이 무한 반복되는 걸 목격하게 된다. 이 현상의 공통 원인은 하나다: useEffect가 실행될 때마다 의존성이 새로 만들어진다.
원인 분석: 왜 무한루프가 발생하는가
원인 1 — 의존성 배열 생략
// ❌ 잘못된 코드
useEffect(() => {
fetchData().then(res => setData(res));
}); // 의존성 배열 없음 → 매 렌더마다 실행
의존성 배열이 없으면 컴포넌트가 렌더링될 때마다 effect가 실행된다. setData가 상태를 바꾸고 → 렌더링 발생 → effect 재실행 → 무한반복.
원인 2 — 객체/배열을 의존성으로 사용
// ❌ 잘못된 코드
const filters = { page: 1, size: 10 }; // 렌더마다 새 객체 생성
useEffect(() => {
fetchList(filters);
}, [filters]); // filters는 매 렌더마다 새 참조 → 매번 실행
JavaScript에서 객체와 배열은 참조 비교다. { page: 1 } === { page: 1 }은 false다. 컴포넌트가 렌더링될 때마다 filters는 메모리상 새 객체로 생성되므로 의존성이 항상 "바뀐 것"으로 판단된다.
원인 3 — 함수를 의존성으로 사용
// ❌ 잘못된 코드
const fetchData = async () => {
const res = await api.get('/data');
setData(res);
};
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData도 매 렌더마다 새 함수 참조
컴포넌트 본문에서 선언된 함수는 렌더링마다 새로 생성된다. useCallback 없이 함수를 의존성에 넣으면 역시 무한루프다.
원인 4 — state를 읽고 다시 set하는 패턴
// ❌ 잘못된 코드
useEffect(() => {
setCount(count + 1); // count 읽기 → setCount → 렌더 → count 변경 → 재실행
}, [count]);
해결방법
해결 1 — 마운트 시 한 번만 실행: 빈 배열 []
// ✅ 올바른 코드
useEffect(() => {
fetchData().then(res => setData(res));
}, []); // 컴포넌트 마운트 시 한 번만 실행
해결 2 — 객체 대신 원시값을 의존성으로
// ✅ 올바른 코드
const [page, setPage] = useState(1);
const [size] = useState(10);
useEffect(() => {
fetchList({ page, size });
}, [page, size]); // 원시값은 값 비교 → 실제 변경 시에만 실행
해결 3 — useMemo로 객체 메모이제이션
// ✅ 올바른 코드
const filters = useMemo(() => ({ page, size, sort }), [page, size, sort]);
useEffect(() => {
fetchList(filters);
}, [filters]); // filters 참조가 내부 값이 바뀔 때만 새로 생성됨
해결 4 — useCallback으로 함수 참조 안정화
// ✅ 올바른 코드
const fetchData = useCallback(async () => {
const res = await api.get('/data');
setData(res);
}, []); // 의존성이 없으면 마운트 시 한 번만 생성
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData 참조가 안정적이므로 무한루프 없음
해결 5 — 함수형 업데이트로 state 읽기 제거
// ✅ 올바른 코드
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // prev를 통해 읽으므로 count를 deps에 넣을 필요 없음
}, 1000);
return () => clearInterval(timer);
}, []); // count 의존성 제거 → 무한루프 없음
정리표
| 원인 | 증상 | 해결 |
|---|---|---|
| 의존성 배열 생략 | 매 렌더마다 실행 | [] 또는 올바른 deps 추가 |
| 객체/배열을 deps로 사용 | 항상 "바뀐" 것으로 판단 | 원시값 분리 또는 useMemo |
| 함수를 deps로 사용 | 매 렌더마다 새 참조 | useCallback으로 메모이제이션 |
| state 읽고 다시 set | 상태 변경 → 재실행 루프 | 함수형 업데이트 사용 |
| 부모 컴포넌트 렌더 | 자식 deps 객체 재생성 | React.memo + useMemo 조합 |
ESLint 경고를 무시하지 말 것
eslint-plugin-react-hooks의 exhaustive-deps 규칙이 경고를 띄우면 반드시 이유를 파악하라. // eslint-disable-next-line으로 무시하면 대부분 나중에 무한루프나 stale closure 버그로 돌아온다.
무한루프 문제는 결국 "이 effect는 언제 실행되어야 하는가"라는 설계 질문이다. 의존성 배열은 React에게 그 의도를 전달하는 계약서다. 참조 동일성과 값 동일성의 차이를 이해하면 대부분의 무한루프는 자연스럽게 해결된다.
댓글 없음:
댓글 쓰기