🔍 검색 키워드: React useEffect 무한루프, useEffect 의존성 배열, useEffect 성능 최적화, React 렌더링 무한 반복
React useEffect 무한루프 문제 해결
증상
import { useEffect, useState } from 'react';
export default function DataFetcher() {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}); // ⚠️ 의존성 배열 없음
return <div>{data.length} items</div>;
}
결과: 컴포넌트가 무한히 렌더링되고 API 요청이 계속 발생하며 브라우저 성능이 급격히 저하됩니다.
원인
React 18 Strict Mode에서 개발 환경 시 useEffectꊔ 의도적으로 2회 실행되지만, 가장 흔한 무한루프 원인은 의존성 배열이 생략되거나 빈 배열이 아닌 경우입니다.
- 의존성 배열 없음: 렌더링될 때마다 effect가 실행 → state 업데이트 → 다시 렌더링 → 무한 루프 - 객체/배열을 의존성으로 사용: 매 렌더링마다 새 참조 생성 → effect 재실행 → 무한 루프
// ❌ 나쁜 예: effect 내부에서 생성한 객체를 의존성으로 사용
useEffect(() => {
const config = { timeout: 5000 };
setData(config);
}, [config]); // config는 매번 새로 생성됨
// ❌ 나쁜 예: 함수를 의존성으로 사용
const fetchData = () => fetch('/api');
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData는 매번 새로 생성됨
해결방법
1. 의존성 배열 추가 (기본 해결)
import { useEffect, useState } from 'react';
export default function DataFetcher() {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, []); // ✅ 마운트 시에만 실행
return <div>{data.length} items</div>;
}
2. 객체/배열 의존성 안정화
// ❌ useEffect 내부에서 객체 생성
useEffect(() => {
const options = { headers: { 'Authorization': token } };
fetchWithOptions(options);
}, []); // 작동하지만 options는 매번 재생성
// ✅ useMemo로 안정화
const options = useMemo(() =>
({ headers: { 'Authorization': token } }),
[token]
);
useEffect(() => {
fetchWithOptions(options);
}, [options]);
3. 함수 의존성 안정화 (useCallback)
// ❌ fetchData 매번 재생성
const fetchData = () => {
return fetch('/api/data').then(r => r.json());
};
useEffect(() => {
fetchData();
}, [fetchData]);
// ✅ useCallback으로 메모이제이션
const fetchData = useCallback(() => {
return fetch('/api/data').then(r => r.json());
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
4. 실전 예제: API 데이터 페칭
import { useEffect, useState, useCallback } from 'react';
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // 언마운트 후 state 업데이트 방지
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(/api/users/${userId});
const data = await response.json();
if (isMounted) {
setUser(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setUser(null);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchUser();
return () => {
isMounted = false; // 정리 함수: 언마운트 시 플래그 설정
};
}, [userId]); // userId 변경 시에만 재실행
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user?.name}</div>;
}
정리표
| 상황 | 원인 | 해결책 |
|---|---|---|
| 의존성 배열 없음 | 매 렌더링마다 effect 재실행 | [] 추가하거나 올바른 의존성 지정 |
| 객체를 의존성으로 사용 | 참조 변경으로 매번 재실행 | useMemo로 메모이제이션 |
| 함수를 의존성으로 사용 | 새 함수 참조로 무한 루프 | useCallback으로 메모이제이션 |
| state setter 직접 의존성 | 불필요한 재실행 | 의존성에서 제거 (setter는 안정함) |
| 외부 변수를 직접 사용 | ESLint 경고 무시 시 버그 | 모든 의존성을 배열에 포함 |