useEffect의 의존성 배열 거짓말: Stale Closure 문제 해결과 useLayoutEffect가 필요한 순간

크리스가 만든 타이머 앱이 이상하다. 1초마다 숫자가 올라가야 하는데, 1에서 멈춰버리거나, 갑자기 숫자가 널뛰기를 한다.
// ❌ 버그가 있는 타이머
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 계속 0만 출력된다!
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 의존성 배열이 비어있다 (문제의 원인)
return <div>{count}</div>;
}VS Code의 린터(Linter)는 노란 줄을 띄우며 경고한다.
"React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array."
하지만 크리스는 생각한다.
"아니, count를 넣으면 useEffect가 계속 재실행돼서 타이머가 초기화되잖아? 그냥 무시해야지."
이 순간이 바로 리액트 초보자가 가장 많이 빠지는 함정, 의존성 배열의 거짓말과 Stale Closure(상한 클로저) 문제가 발생하는 지점이다.
1. 거짓말의 대가: Stale Closure
위 코드에서 setInterval 내부의 화살표 함수는 컴포넌트가 처음 렌더링 될 때 생성되었다.
이때의 count 값은 0이다. 이 함수는 영원히 count가 0인 세계(클로저)에 갇혀 있다.
이것이 바로 Stale Closure다. 오래된(Stale) 변수를 참조하고 있는 것이다.
해결책 1: 의존성 배열을 사실대로 말하기
린터의 말을 잘 들으면 된다. 하지만 setInterval이 매번 초기화되는 문제가 생긴다.
// ⚠️ 동작은 하지만 비효율적임
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]); // count가 바뀔 때마다 타이머 해제 -> 재설정 반복해결책 2: 함수형 업데이트 (Best Practice)
count 값을 직접 읽지 않고, 리액트에게 "현재 값에서 1 더해줘"라고 명령만 내리면 된다.
// ✅ 의존성 배열도 깔끔하고, 타이머도 유지됨
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // count를 의존하지 않음!
}, 1000);
return () => clearInterval(id);
}, []);2. useLayoutEffect가 필요한 순간
보통의 사이드 이펙트는 useEffect로 충분하다. 하지만 화면 깜빡임이 발생하는 경우가 있다.
상황: 툴팁의 위치를 계산해야 한다. 툴팁을 일단 렌더링하고(useEffect), 그 높이를 잰 다음, 위치를 조정(setState)한다.
사용자는 툴팁이 순간이동 하는 것을 목격하게 된다. 이때 필요한 것이 useLayoutEffect다.
useLayoutEffect 내부의 코드는 브라우저가 화면을 그리기(Paint) 직전에 동기적으로 실행된다. 여기서 DOM을 조작하면, 사용자는 최종적으로 계산된(이동된) 화면만 보게 된다.
// tooltip.tsx
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setY(height + 10);
// 이 계산이 끝날 때까지 브라우저는 화면을 그리지 않고 기다린다.
}, []);핵심 정리
이제 훅의 함정을 피하는 법을 배웠다. 그런데 상태가 단순히 숫자 하나가 아니라, 복잡한 객체나 여러 개의 값이 얽혀 있다면? useState만으로는 감당하기 힘들 때가 온다.
"useState vs useReducer: 상태가 복잡해지는 순간 선택해야 할 도구" 편에서 계속된다.