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

2025. 9. 7. 20:56·Frontend Development
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
'Frontend Development' 카테고리의 다른 글
  • 실무에서 꼭 알아야 할 JWT 저장소 보안 패턴과 공격 탐지 방법
  • Next.js SSR 페이지 풀 페이지 캐싱
  • React Error Boundary: 왜 아직도 클래스일까?
  • setTimeout vs Promise.then vs queueMicrotask
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (133)
      • Frontend Development (62)
      • Backend Development (25)
      • Algorithm (33)
        • 백준 (11)
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
      • Language (6)
        • JavaScript (6)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    tailwindcss
    frontend development
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
useEffect에서 setInterval이 상태를 못 따라오는 이유 (stale closure)
상단으로

티스토리툴바