728x90
리액트에서 useEffect
와 setInterval
을 함께 쓰다 보면, 분명 1초마다 증가시키라고 했는데 상태가 갱신되지 않거나 0에 멈춰 있는 현상을 자주 만납니다. 원인은 대부분 “stale closure(오래된 클로저)” 입니다. 핵심만 간단히 정리합니다.
TL;DR
- 문제 원인: 빈 의존성 배열
[]
로 등록한 이펙트는 최초 렌더의count
를 클로저로 캡처합니다. 그 뒤 타이머 콜백은 계속 “초기값”만 봅니다. - 정석 해결: 다음 상태가 이전 상태에 의존하면, 항상 함수형 업데이트
setState(prev => ...)
를 사용하세요. - 필수: 타이머는 반드시 정리(cleanup)합니다.
문제 코드
import { useEffect, useState } from 'react'
export default function CounterBroken() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1) // 초기 렌더의 count(=0)만 본 채 고정
}, 1000)
return () => clearInterval(id)
}, [])
return <div>Count: {count}</div>
}
원인: stale closure
의존성 배열에 count
를 넣지 않으면, 이펙트 안의 콜백은 최초 렌더 시점의 count
를 닫아 캡처합니다. 이후 재렌더가 일어나도 콜백이 참조하는 count
는 업데이트되지 않습니다.
해결 1: 함수형 업데이트 사용(권장)
다음 상태가 이전 상태에 기반하면 함수형 업데이트가 가장 간결하고 안전합니다. 의존성 배열을 []
로 두어도 안전합니다.
import { useEffect, useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1)
}, 1000)
return () => clearInterval(id)
}, [])
return <div>Count: {count}</div>
}
해결 2: 의존성에 상태를 넣고 정리하기(대안)
상태를 의존성에 포함하면, 이펙트가 매 업데이트마다 재등록되며 “최신 상태”를 참조합니다. 단, 매번 타이머가 재생성되니 필요에 따라 setTimeout
패턴이 더 적합할 수 있습니다.
import { useEffect, useState } from 'react'
export default function CounterWithDeps() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setTimeout(() => setCount(count + 1), 1000)
return () => clearTimeout(id)
}, [count])
return <div>Count: {count}</div>
}
Best Practice
- 이전 상태 의존 시 함수형 업데이트:
setX(prev => ...)
는 stale closure 문제를 깔끔히 해결합니다. - 항상 정리:
setInterval
/setTimeout
은 반드시 cleanup으로 해제합니다. - 불필요한 재생성 피하기: 단순 누적 카운트에는 해결 1이 더 단순하고 정확합니다.
- 제어가 필요한 경우 ref 활용: 일시정지/재개 등 제어가 필요하면
useRef
에 타이머 id를 보관하세요.
한 줄 요약
“상태가 이전 값에 의존하면 함수형 업데이트를 쓰고, 타이머는 반드시 정리한다.” 이것만 지키면 useEffect + 타이머
의 대부분 문제를 피할 수 있습니다.
728x90
'Frontend Development' 카테고리의 다른 글
실무에서 꼭 알아야 할 JWT 저장소 보안 패턴과 공격 탐지 방법 (0) | 2025.09.13 |
---|---|
Next.js SSR 페이지 풀 페이지 캐싱 (0) | 2025.09.08 |
React Error Boundary: 왜 아직도 클래스일까? (0) | 2025.09.07 |
setTimeout vs Promise.then vs queueMicrotask (0) | 2025.09.05 |
Next.js 최신 캐싱 전략 총정리 (0) | 2025.09.03 |