목요일

React useEffect 무한루프 완벽 해결 가이드 — 의존성 배열 원인과 해결방법

🔍 검색 키워드: 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-hooksexhaustive-deps 규칙이 경고를 띄우면 반드시 이유를 파악하라. // eslint-disable-next-line으로 무시하면 대부분 나중에 무한루프나 stale closure 버그로 돌아온다.

무한루프 문제는 결국 "이 effect는 언제 실행되어야 하는가"라는 설계 질문이다. 의존성 배열은 React에게 그 의도를 전달하는 계약서다. 참조 동일성과 값 동일성의 차이를 이해하면 대부분의 무한루프는 자연스럽게 해결된다.

댓글 없음:

댓글 쓰기