useEffect를 데이터 동기화로 이해하기
useEffect를 외부 세계 동기화 도구로 보고 의존성·cleanup·무한 루프를 해설
useEffect는 사이드 이펙트가 아니라 “데이터 동기화 도구”다
useEffect는 React에서 가장 많이 오해되는 훅이다.
많은 코드베이스에서 useEffect는 문제 해결용 만능 훅처럼 사용된다.
하지만 이 훅의 본질은 분명하다.
useEffect는 “무언가를 실행하기 위한 도구”가 아니라
React 외부의 세계와 데이터를 동기화하기 위한 도구다.
이 관점을 잡지 못하면, useEffect는 곧 무한 루프, 중복 요청, 타이밍 버그의 근원이 된다.
Side Effect의 정확한 정의
개념
React에서 말하는 side effect란 다음이다.
렌더링 결과를 계산하는 것 이외의 모든 작업
구체적으로:
- 서버 요청(fetch)
- 브라우저 API 접근(localStorage, document, window)
- 타이머(setTimeout, setInterval)
- 외부 상태 시스템과의 동기화
반대로 side effect가 아닌 것:
- JSX 계산
- state/props로부터 값 계산
- 조건 분기, 배열 map/filter
왜 중요한가
React의 렌더링은 순수 함수 호출을 전제로 한다.
UI = f(state, props)
렌더 중에 side effect가 발생하면:
- 렌더링이 몇 번 호출될지 예측 불가
- 동일 입력 → 동일 출력이라는 가정이 깨짐
- Strict Mode에서 버그가 증폭됨
그래서 React는 이렇게 요구한다.
side effect는 렌더 바깥에서, 명시적으로 관리하라 →
useEffect
useEffect의 정체성: “외부 세계와의 동기화”
개념
useEffect는 이렇게 이해해야 한다.
“이 값들이 변하면, React 외부 세계도 이 상태에 맞게 동기화되어야 한다.”
1
2
3
useEffect(() => {
document.title = title;
}, [title]);
이 코드는:
- “title이 바뀔 때마다”
- “브라우저 탭 제목을 title과 동기화한다”
왜 중요한가
이 관점이 잡히면 다음이 명확해진다.
- useEffect는 상태 변화의 결과를 처리한다
- useEffect 안에서 state를 “만들어내는” 건 위험 신호다
- 의존성 배열은 “언제 동기화가 필요한가”에 대한 선언이다
dependency array의 의미: “언제 다시 동기화할 것인가”
개념
의존성 배열은 단순한 최적화 옵션이 아니다.
1
2
3
useEffect(() => {
syncSomething(value);
}, [value]);
이 의미는 정확히 이것이다.
“value가 이전과 달라졌을 때만 이 effect를 다시 실행하라”
왜 중요한가
React는 렌더링을 여러 번 수행할 수 있다.
- 상태 변경
- 부모 렌더
- Strict Mode의 이중 실행
의존성 배열은 렌더 횟수와 effect 실행을 분리한다.
잘못된 이해에서 나오는 패턴
1) 의존성 배열을 “맞추기” 위한 코드
1
2
3
useEffect(() => {
doSomething();
}, []); // 그냥 한 번만 실행하고 싶어서
이 코드는 실제 의미를 숨긴다.
- 정말로 “한 번만” 실행돼야 하는가?
- 아니면 “특정 값과 동기화”되어야 하는가?
빈 배열은:
“이 effect는 어떤 state와도 동기화되지 않는다”
라는 강한 선언이다.
무한 루프가 생기는 근본 원인
핵심 원인
effect 안에서, effect의 의존성을 다시 변경할 때
전형적인 무한 루프 예제
1
2
3
useEffect(() => {
setData(fetchData());
}, [data]);
왜 루프가 도는가
data변경 → 렌더- 렌더 후 effect 실행
- effect 안에서
setData data변경 → 다시 렌더- 반복
이건 React 버그가 아니다. 동기화 대상과 결과를 뒤섞은 설계 오류다.
올바른 사고 방식
- effect의 의존성 = “동기화 기준”
- effect의 결과 = “외부 세계 반영”
state를 만들어내는 로직은:
- 이벤트 핸들러
- reducer
- 사용자 액션
쪽에 있어야 한다.
cleanup 함수: “이전 동기화의 해제”
개념
cleanup은 effect가 다시 실행되기 전 또는 언마운트 시 호출된다.
1
2
3
4
5
6
7
useEffect(() => {
const id = setInterval(tick, 1000);
return () => {
clearInterval(id);
};
}, []);
왜 중요한가
side effect는 대부분 지속성을 가진다.
- 타이머
- 이벤트 리스너
- 소켓 연결
- 구독(subscription)
cleanup이 없으면:
- 메모리 누수
- 중복 실행
- 의도치 않은 상태 업데이트
언제 문제가 되는가
- cleanup을 “언마운트 전용”으로 오해
- 의존성 변경 시 cleanup이 먼저 실행된다는 사실을 무시
- 외부 리소스를 해제하지 않음
기억할 규칙
effect는 “설정(setup)”이고 cleanup은 그 설정의 되돌리기다.
useEffect가 필요 없는 경우 (가장 중요)
1) 파생 값 계산
1
const fullName = first + last;
→ effect 필요 없음 → 렌더링 중 계산이 정답
2) 이벤트에 따른 state 변경
1
onClick={() => setCount(c => c + 1)}
→ 사용자 액션은 effect 대상이 아니다
3) props/state 변화에 따른 UI 분기
1
{isLoading && <Spinner />}
→ 선언적 렌더링 영역
4) 초기값 설정
1
const [value] = useState(props.initialValue);
→ effect로 복사 금지 → props → state 동기화는 대부분 설계 문제
useEffect 사용 여부 판단 체크리스트
다음 질문에 “예”가 아니면 effect가 필요 없다.
- React 외부의 무언가를 다루는가?
- 그 외부 대상이 state/props 변화와 동기화되어야 하는가?
- 렌더링 계산만으로 표현할 수 없는가?
정리: useEffect를 잘 쓰는 사람의 사고방식
- useEffect는 실행 훅이 아니다
- 데이터 동기화 선언이다
- dependency array는 “언제”에 대한 선언
- cleanup은 “되돌리기”
- 무한 루프는 동기화 대상과 결과를 혼동한 설계 오류
한 문장으로 요약하면 이것이다.
useEffect는 React를 벗어난 세계를 React의 상태 변화에 맞춰 정렬하는 도구다.
