Frontend Development

useEffect에서 setInterval이 상태를 못 따라오는 이유 (stale closure)

Kun Woo Kim 2025. 9. 7. 20:56
728x90

리액트에서 useEffectsetInterval을 함께 쓰다 보면, 분명 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