모든 글

[배우는 중] 화면 숫자는 3인데 콘솔엔 0만 찍혔다 — 오래된 클로저

setInterval을 useEffect에 넣었더니 state가 바뀌어도 콜백은 계속 옛날 값만 봤다. "오래된 클로저(stale closure)"에 데이고, 왜 그런지와 빠져나온 길을 남긴다.

배우는 중(Learning log) — 완성된 정리가 아니라, 혼자 공부하다 막힌 지점과 그걸 넘은 과정을 그대로 남기는 시리즈입니다. 지금 제 이해라서 틀릴 수 있고, 나중에 알게 되면 정정 노트를 답니다.

1초마다 카운트를 콘솔에 찍는 타이머 하나.

별거 아니라고 생각했어요.

function Timer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => console.log(count), 1000);
    return () => clearInterval(id);
  }, []);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

버튼을 세 번 눌렀어요.

화면 숫자는 3.

그런데 콘솔은 1초마다 계속 0, 0, 0…

분명 count는 바뀌었는데, setInterval 안의 count만 시간이 멈춘 것 같았어요.

이런 버그가 제일 곤란해요.

화면은 멀쩡하니까, 뭐가 틀렸는지 눈에 안 보이거든요.

한참 뒤에야 이유를 알았습니다.

React 함수 컴포넌트는 렌더될 때마다 함수 전체가 처음부터 다시 실행돼요.

그 순간의 count 값이 그 실행 안에 박제되고요.

useEffect에 넘긴 콜백도 첫 렌더 때의 count(=0)를 붙잡은 채, 빈 배열 [] 때문에 다시 만들어지지도 않아요.

그 콜백은 영원히 첫 렌더의 세계에 삽니다.

사진 같은 거예요.

찍는 순간 인화돼서, 실제 인물이 나이를 먹어도 사진 속 얼굴은 그대로인.

이걸 오래된 클로저(stale closure)라고 부르더라고요.

빠져나오는 길은 세 가지였어요.

하나는 의존성 배열에 count를 넣는 것.

count가 바뀔 때마다 effect가 새로 걸리며 콜백도 최신으로 갱신돼요.

(ESLint의 react-hooks/exhaustive-deps가 이걸 잔소리해줍니다.)

아니면 값을 내가 읽지 않는 것.

setCount(c => c + 1);

“이전 값에 1 더해줘”라고 React에 맡기면, 클로저에 붙잡힌 옛 count가 끼어들 자리가 없어요.

마지막은 useRef에 최신 값을 담아 .current로 읽는 것.

ref는 렌더와 상관없이 같은 상자를 가리키니까요.

돌아보니 제 착각은 단순했어요.

”참조하면 당연히 최신 값이겠지.”

함수 안에 잡힌 변수는 그 함수가 만들어진 순간의 값인데 말이죠.

리렌더는 매번 새 함수를 만들 뿐이고요.

예전에 캡처해둔 스크린샷을 붙여놓고 “왜 실시간이 아니지?”

하던 거랑 비슷했어요.

useEffectEvent라는 정식 해법이 있다던데, 그건 아직 안 써봤어요.

혹시 state는 바뀌었는데 콜백 안에서만 옛날 값이면 — 그 콜백의 의존성 배열부터 보세요.

N
NACRE
Writing on less · Seoul
다른 글 보기

댓글

  1. 불러오는 중…