useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가
React를 어느 정도 사용하다 보면 한 번쯤은
“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다.
useEffect가 계속 늘어나고
의존성 배열은 점점 길어지고
왜 실행되는지 이해하기 어려워지며
eslint-disable-next-line 이 늘어나는 상태
하지만 많은 사람들이 오해한다.
useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다.
이 글에서는
‘useEffect 지옥’의 정확한 의미
무엇이 진짜 문제인지
실무에서 안전하게 사용하는 패턴은 무엇인지
를 정리한다.
useEffect 지옥의 정확한 의미
useEffect 지옥이란
컴포넌트의 동작 흐름이 state 변화와 effect에 암묵적으로 묶여
코드만 보고는 “언제, 왜 실행되는지” 이해할 수 없는 상태다.
핵심은 개수도 아니고, 의존성 배열의 길이도 아니다.
useEffect(() => subscribe(), [])
useEffect(() => fetchUser(id), [id])
useEffect(() => trackPage(path), [path])
이처럼 effect가 여러 개여도
각 effect의 역할이 명확하고
서로 영향을 주지 않는다면
전혀 지옥이 아니다.
흔한 오해들
❌ useEffect가 많으면 지옥이다?
아니다.
“이 effect는 무슨 역할인가?”를 한 문장으로 설명할 수 있다면 문제 없다.
❌ 의존성 배열이 길면 지옥이다?
아니다.
문제는 여러 원인 → 여러 부작용을 한 effect에서 처리할 때다.
// ❌ 위험한 패턴
useEffect(() => {
if (a) setX(...)
if (b) fetch(...)
if (c) setY(...)
}, [a, b, c])
이 경우, 실행 이유와 결과가 섞이면서 추론이 어려워진다.
useEffect 지옥의 진짜 원인
1. 파생 상태를 state + effect로 관리할 때
// ❌
useEffect(() => {
setTotal(price * count)
}, [price, count])
이런 코드는
선언적인 계산을
명령형 사이드 이펙트로 바꿔버린다.
// ✅
const total = price * count
2. 이벤트를 effect로 처리할 때
// ❌
useEffect(() => {
if (clicked) submit()
}, [clicked])
사용자 액션은 이벤트 핸들러에서 직접 처리해야 한다.
effect는 자동 실행되기 때문에 제어 흐름을 흐리게 만든다.
3. effect들이 서로를 트리거할 때
useEffect(() => {
setA(...)
}, [b])
useEffect(() => {
setB(...)
}, [a])
이런 구조에서는 실행 순서가 코드에 드러나지 않는다.
렌더링 사이클을 머릿속에서 시뮬레이션해야 한다.
그럼 useEffect는 언제 “안전한가”?
useEffect는 오직 React 외부 세계와의 동기화에만 사용될 때 안전하다.
안전한 useEffect 사용 예시
1. API 요청 (서버 상태 동기화)
useEffect(() => {
let cancelled = false
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data)
})
return () => {
cancelled = true
}
}, [userId])
입력(userId) ↔ 서버 데이터 동기화
effect의 존재 이유가 명확
2. 이벤트 리스너 등록 / 해제
useEffect(() => {
const onResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
브라우저 API는 React 외부 세계
cleanup이 명확한 패턴
3. 타이머 관리
useEffect(() => {
const id = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(id)
}, [])
타이머 생명주기 = 컴포넌트 생명주기
effect의 역할이 명확
4. WebSocket / 구독 관리
useEffect(() => {
const socket = new WebSocket(url)
socket.onmessage = e => {
setMessages(prev => [...prev, JSON.parse(e.data)])
}
return () => socket.close()
}, [url])
연결/해제 관리
외부 리소스 동기화의 전형적인 예
5. DOM 직접 제어
useEffect(() => {
inputRef.current?.focus()
}, [])
렌더 이후에만 가능한 작업
effect가 아니면 해결 불가
안전한 useEffect 판단 체크리스트
다음 질문에 모두 YES면 안전하다.
이 effect는 React 외부의 무언가를 다루는가?
이 effect를 제거하면 UI 선언만으로는 불가능한가?
effect 하나에 책임이 하나뿐인가?
실행 이유를 한 문장으로 설명할 수 있는가?
결론
useEffect 지옥은
useEffect를 ‘동기화 도구’가 아니라
‘제어 흐름 도구’로 사용하면서 시작된다.
개수는 문제가 아니다
의존성 배열도 문제가 아니다
역할이 섞인 effect가 문제다
useEffect는 줄이는 게 목표가 아니라,
쓸 이유가 분명한 곳에만 쓰는 것이 목표다.
🧾 작성 참고
이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.