React 개발을 하다 보면 한 번쯤은 마주치게 되는 문제가 있습니다. 바로 useEffect 무한루프입니다.
"어? 왜 API가 계속 호출되지?" "브라우저가 멈췄어요!" "개발자 도구 네트워크 탭이 빨간색으로 가득해요!"
혹시 이런 경험 있으신가요? 저도 React를 처음 배울 때 이 문제로 몇 시간을 헤맸던 기억이 납니다. 오늘은 이 골치 아픈 무한루프 문제를 완전히 해결하는 방법을 알려드리겠습니다.
🎯 무한루프란 무엇인가?
무한루프는 말 그대로 끝없이 반복되는 루프를 의미합니다. React에서는 주로 useEffect가 계속해서 실행되는 상황을 말합니다.
💥 실제 발생한 문제 사례
최근에 사용자 인증 기능을 구현하다가 이런 코드를 작성했습니다:
function Home() {
const [isCheckingProfile, setIsCheckingProfile] = useState(false);
const { user, loading: authLoading } = useAuth();
useEffect(() => {
if (user && !isCheckingProfile) {
setIsCheckingProfile(true); // 👈 문제의 시작
checkUserProfileAndRedirect();
}
}, [user, authLoading, isCheckingProfile]); // 👈 여기가 문제!
return <div>홈 화면</div>;
}
결과는? 브라우저가 먹통이 되었습니다! 😱
🔍 React 컴포넌트 실행 순서 이해하기
무한루프 문제를 해결하려면 먼저 React 컴포넌트가 어떤 순서로 실행되는지 알아야 합니다.
📋 컴포넌트 생명주기
export default function Home() {
// 1️⃣ 먼저 실행 - useState, 커스텀 훅 등
const [showSplash, setShowSplash] = useState(true);
const [isCheckingProfile, setIsCheckingProfile] = useState(false);
const { user, loading: authLoading } = useAuth(); // 커스텀 훅도 여기서 실행
const router = useRouter();
// 2️⃣ 나중에 실행 - useEffect (컴포넌트 렌더링 후)
useEffect(() => {
// 이 코드는 컴포넌트가 화면에 그려진 후 실행됨
}, [user, authLoading]);
// 3️⃣ return 문 (렌더링)
return <div>...</div>;
}
🎭 상세 실행 단계
순서 단계 설명
| 1단계 | 함수 실행 | useState, 커스텀 훅 등 초기화 |
| 2단계 | 렌더링 | return 문의 JSX가 DOM에 그려짐 |
| 3단계 | useEffect 실행 | 부수 효과(side effect) 처리 |
핵심 포인트: useEffect는 항상 렌더링 이후에 실행됩니다!
🌀 무한루프가 발생하는 이유
🔄 useEffect 동작 원리
useEffect(() => {
// 실행할 코드
}, [의존성1, 의존성2]); // 👈 이 값들 중 하나라도 바뀌면 재실행
useEffect는 의존성 배열의 값이 변경될 때마다 다시 실행됩니다. 이게 바로 무한루프의 원인이 됩니다.
💀 무한루프 발생 과정
앞서 본 문제 코드를 다시 살펴보겠습니다:
useEffect(() => {
if (user && !isCheckingProfile) {
setIsCheckingProfile(true); // 👈 상태 변경!
checkUserProfileAndRedirect();
}
}, [user, authLoading, isCheckingProfile]); // 👈 변경된 상태가 의존성에 포함됨
무한루프 단계별 분석:
1. isCheckingProfile: false → true 변경
↓
2. 의존성 배열에 isCheckingProfile이 있어서 useEffect 재실행 🔄
↓
3. 조건문 통과 못함 (이미 true니까)
↓
4. finally에서 다시 false로 변경
↓
5. 또 useEffect 재실행... 무한 반복! 🌀
🛠️ 해결 방법
✅ 1. 의존성 배열에서 문제 요소 제거
가장 간단한 해결책은 useEffect 내부에서 변경하는 state를 의존성 배열에서 제거하는 것입니다.
// ❌ 문제가 있는 코드
useEffect(() => {
if (user && !isCheckingProfile) {
setIsCheckingProfile(true);
checkUserProfileAndRedirect();
}
}, [user, authLoading, isCheckingProfile]); // isCheckingProfile 때문에 무한루프
// ✅ 해결된 코드
useEffect(() => {
if (user && !isCheckingProfile) {
setIsCheckingProfile(true);
checkUserProfileAndRedirect();
}
}, [user, authLoading]); // isCheckingProfile 제거!
✅ 2. useCallback으로 함수 최적화
함수가 의존성 배열에 있을 때도 무한루프가 발생할 수 있습니다:
// ❌ 무한루프 위험
function UserList() {
const [users, setUsers] = useState([]);
const fetchUsers = () => {
api.getUsers().then(setUsers);
};
useEffect(() => {
fetchUsers(); // 함수가 매번 새로 생성되어 무한루프
}, [fetchUsers]);
return <div>{/* 렌더링 */}</div>;
}
// ✅ useCallback으로 해결
function UserList() {
const [users, setUsers] = useState([]);
const fetchUsers = useCallback(() => {
api.getUsers().then(setUsers);
}, []); // 의존성이 없으므로 함수가 재생성되지 않음
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
return <div>{/* 렌더링 */}</div>;
}
✅ 3. 함수형 업데이트 사용
이전 state 값을 기반으로 업데이트할 때는 함수형 업데이트를 사용하세요:
// ❌ 무한루프 위험
useEffect(() => {
setCount(count + 1);
}, [count]); // count가 변경될 때마다 실행 → 무한루프
// ✅ 함수형 업데이트로 해결
useEffect(() => {
setCount(prev => prev + 1); // 이전 값 기반 업데이트
}, []); // 의존성 없음
📝 의존성 배열 규칙
🟢 포함해야 하는 것
- useEffect 내부에서 읽기만 하는 state, props
- useEffect 내부에서 사용하는 외부 함수, 변수
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) { // userId는 읽기만 하므로 의존성에 포함
fetchUser(userId).then(setUser);
}
}, [userId]); // ✅ 올바른 의존성
return <div>{user?.name}</div>;
}
🔴 포함하지 말아야 하는 것
- useEffect 내부에서 변경하는 state
- setter 함수들 (useState의 반환값)
function Counter() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true); // loading을 변경하지만 의존성에 포함하지 않음
setTimeout(() => {
setCount(prev => prev + 1); // count를 변경하지만 의존성에 포함하지 않음
setLoading(false);
}, 1000);
}, []); // ✅ 변경하는 state들은 의존성에 포함하지 않음
return <div>{loading ? 'Loading...' : count}</div>;
}
🎯 실전 예제
나의 경우엔 다음과 같은 로직으로 무한 루프가 발생했었다.

1️⃣ 로그인 하기 전 (초기 렌더링)
showSplash, isCheckingProfile 선언
→ useAuth 실행(hook도 실행됨)
→ 렌더링 (return 이후 로직 실행)
2️⃣ 로그인 후 -> user 가 있음 -> useEffect 실행
showSplash, isCheckingProfile 선언
→ useAuth 실행
→ 렌더링
→ useEffect 실행 → setIsCheckingProfile(true) → checkUserProfileAndRedirect()
→ router.push('/dashboard') 실행 → finally { setIsCheckingProfile(false) } 여기가 문제!
→ isCheckingProfile 변경 (true → false)
useEffect 의존성 배열 [user, authLoading, isCheckingProfile] 때문에 다시 실행
→ 다시 처음 부터 실행. user && !isCheckingProfile (true) → setIsCheckingProfile(true) 에 의해 또 실행
→ 무한 반복...
🛡️ 무한루프 방지 체크리스트
개발할 때 이 체크리스트를 확인해보세요:
✅ 의존성 배열 점검
- [ ] useEffect 내부에서 변경하는 state가 의존성에 포함되어 있지 않은가?
- [ ] setter 함수가 의존성에 포함되어 있지 않은가?
- [ ] 객체나 배열을 직접 의존성에 포함하지 않았나?
✅ 함수 최적화 점검
- [ ] useEffect에서 사용하는 함수가 useCallback으로 최적화되어 있는가?
- [ ] 불필요하게 매번 새로 생성되는 함수는 없는가?
✅ 로직 구조 점검
- [ ] 한 번만 실행되어야 하는 로직이 반복 실행되고 있지 않은가?
- [ ] 조건부 실행이 제대로 구현되어 있는가?
🔧 디버깅 팁
1. console.log 활용
useEffect(() => {
console.log('useEffect 실행됨', { user, authLoading, isCheckingProfile });
if (user && !isCheckingProfile) {
console.log('인증 확인 시작');
setIsCheckingProfile(true);
checkUserProfileAndRedirect();
}
}, [user, authLoading]); // 의존성도 콘솔에 출력
2. React Developer Tools 사용
React Developer Tools의 Profiler 탭을 사용하면 컴포넌트가 언제, 왜 리렌더링되는지 확인할 수 있습니다.
3. useEffect 분리
복잡한 useEffect는 여러 개로 분리해서 어떤 부분에서 문제가 발생하는지 파악하기 쉽게 만드세요.
💡 마무리
React useEffect 무한루프는 처음엔 어려워 보이지만, 원리를 이해하면 충분히 해결할 수 있는 문제입니다.
핵심 원칙들을 다시 정리하면:
- useEffect 내부에서 변경하는 state는 의존성에 포함하지 않기
- 함수는 useCallback으로 최적화하기
- 객체/배열보다는 원시값을 의존성에 사용하기
- 관련된 로직은 하나의 effect로 통합하기
이 원칙들만 지키셔도 대부분의 무한루프 문제를 예방할 수 있습니다.
무한루프 때문에 고생하고 계신 분들에게 이 글이 도움이 되었으면 좋겠습니다. React 개발이 더욱 즐거워지시길 바랍니다! 🚀
이 글이 도움이 되셨다면 ❤️ 공감과 댓글로 응원해주세요! 더 좋은 개발 팁으로 찾아뵙겠습니다.
'Tech Notes' 카테고리의 다른 글
| 딥링크(Deep Link)란 무엇인가? - 모바일 앱과 웹의 연결고리 (0) | 2025.09.09 |
|---|---|
| 쿠키, localStorage, SessionStorage 완벽 비교 가이드 (2) | 2025.08.25 |
| DB 기반 캐싱 vs 메모리 캐싱: 언제, 어떻게 써야 할까? (3) | 2025.08.15 |
| Next.js App Router 실행 원리 완전 가이드 (1) | 2025.08.15 |
| SQL 쿼리 작성 방식 비교 - 파라미터 바인딩 vs 템플릿 리터럴 (0) | 2025.08.12 |