본문 바로가기
Q&A2026년 4월 13일1분 읽기

React useEffect 안의 setInterval이 state 최신값을 못 읽습니다 (stale closure)

코
코딩하는민수
조회 581댓글 3

카운트다운 훅을 만들고 있는데, setInterval 안에서 찍는 count 값이 항상 0만 찍힙니다.

function useCountdown() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 항상 0
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return count;
}

setCount(c => c + 1)로 업데이트는 정상 동작하는데, 콘솔에 찍는 count만 0 그대로예요. deps에 count를 넣으면 interval이 매초 재생성돼서 원래 의도가 아니고요.

deps 안 건드리고 최신값 참조하는 방법이 있나요?

댓글 3

김김영삼6개월 전

전형적인 stale closure입니다. effect가 처음 실행될 때 캡처된 count=0가 interval 콜백 안에 박혀버리는 구조예요.


패턴 ①: useRef로 최신값 미러링

deps를 비운 채 최신값만 읽고 싶을 때 쓰는 공식 패턴에 가깝습니다.


function useCountdown() {
const [count, setCount] = useState(0);
const countRef = useRef(count);

useEffect(() => { countRef.current = count; }, [count]);

useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // 최신값
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);

return count;
}

패턴 ②: Dan Abramov의 useInterval 훅

로직을 추상화한 커스텀 훅입니다. 콜백을 ref에 담아 항상 최신 버전을 호출합니다.


function useInterval(callback, delay) {
const saved = useRef(callback);
useEffect(() => { saved.current = callback; }, [callback]);
useEffect(() => {
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]);
}

왜 setCount(c => c+1)은 됐고 console.log는 안 됐나

setCount의 updater 함수는 React가 호출 시점의 최신 state를 인자로 주입하기 때문입니다. 반면 console.log(count)는 effect 클로저에 캡처된 값 그대로라 0이 박힌 거죠. 이 구분이 이 버그를 이해하는 핵심입니다.

프프론트수진6개월 전

저도 이 버그 몇 번 겪고 나서는 "interval/subscription/eventListener 안에서 state 읽어야 하면 일단 ref 미러링" 규칙으로 갔어요. React 공식 문서 "Separating Events from Effects" 챕터에 useEffectEvent 얘기도 있는데 아직 experimental이라 프로덕션에서는 useRef 쪽이 안전합니다.

코코딩하는민수6개월 전

useRef 미러링 패턴으로 해결됐습니다. setCount가 왜 됐는지도 이해가 명확해졌어요. 감사합니다!

Ctrl+Enter로 등록