useEffect cleanup과 타이머 — 의존성 배열이 지우는 예약
개인적으로 만든 React 웹 앱에 첫 화면 인트로를 넣었더니 별이 하나도 안 켜졌습니다. 에러도 없고 화면이 죽지도 않고, 그냥 아무 일도 일어나지 않았습니다.
캔버스에 별을 그려 두고 순서대로 하나씩 켜지는 연출이고, 구현은 두 갈래로 나눠 뒀습니다. 그림을
계속 다시 그리는 쪽은 requestAnimationFrame 으로 매 프레임 캔버스를 갱신하고, 언제 어느 별을
켤지는 setTimeout 을 여러 개 미리 예약해 두고 시간이 되면 상태를 바꾸는 식으로 잡았습니다.
인트로가 어디까지 진행됐는지는 localPhase 라는 상태로 들고 있고, 이 값이 바뀌면 그리는 내용도
바뀝니다.
타이머까지 정리한 애니메이션 effect
문제는 useEffect cleanup 이었습니다.
useEffect(() => {
animRef.current = requestAnimationFrame(draw)
return () => {
cancelAnimationFrame(animRef.current)
timersRef.current.forEach(clearTimeout) // ← 여기
}
}, [localPhase])
이 effect 는 캔버스 애니메이션을 시작하고 cleanup 에서 애니메이션 프레임을 취소합니다. 그런데 그
cleanup 이 타이머(timersRef)까지 같이 지웁니다.
여기에 의존성 배열 [localPhase] 가 같이 작용합니다. localPhase 가 바뀔 때마다 React 는 이전
effect 의 cleanup 을 돌리고 effect 를 다시 실행했습니다. 별을 켜는 과정 자체가 phase 를 바꾸다
보니 그때마다 cleanup 이 돌면서 아직 실행도 안 된 타이머들을 전부 clearTimeout 해 버립니다.
별을 켜려고 예약한 타이머가 phase 가 바뀌는 순간 다 취소돼서 별이 안 켜졌는데, 에러도 안 났습니다 — 타이머가 조용히 사라진 것뿐이었습니다.
cleanup 대상 분리
애니메이션 프레임은 phase 가 바뀌면 취소하는 게 맞지만, 타이머는 phase 전환을 넘어서 살아남아야 하니, 둘을 섞어놨던 게 문제였습니다.
그래서 애니메이션 effect 는 프레임만 정리하게 남깁니다.
useEffect(() => {
animRef.current = requestAnimationFrame(draw)
// 여기서는 애니메이션 프레임만 취소한다 — 타이머는 phase 전환을 넘어 살아야 한다
return () => cancelAnimationFrame(animRef.current)
}, [localPhase])
타이머 정리는 컴포넌트가 사라질 때만 하도록 별도 effect 로 뺍니다. 의존성 배열이 비어
있으니([]) 이 cleanup 은 unmount 때 딱 한 번만 돕니다.
// 언마운트 때만 타이머 정리
useEffect(() => {
return () => { timersRef.current.forEach(clearTimeout) }
}, [])
새 챌린지를 시작할 때는 이전에 남은 타이머가 있으면 그때 비웁니다.
const startChallenge = () => {
timersRef.current.forEach(clearTimeout)
timersRef.current = []
// ... 별 켜는 타이머 예약
}
이렇게 나누고 나니 타이머의 수명이 분명해졌습니다. 새 챌린지를 시작할 때와 컴포넌트가 사라질 때만 비우고, phase 전환은 타이머를 안 건드립니다.
effect 분리 기준 — 정리 대상의 수명
useEffect 의 cleanup 은 “이 effect 가 만든 것” 만 정리해야 합니다. 애니메이션 effect 의
cleanup 에 타이머 정리를 끼워 넣은 게 실수였습니다. 그 effect 는 localPhase 마다 다시 돌지만,
타이머는 그 주기와 수명이 다릅니다.
의존성 배열이 있는 effect 의 cleanup 은 “그 의존성이 바뀔 때마다” 돈다는 걸 잊기 쉽습니다. 수명이 서로 다른 것들을 한 effect 에 묶으면, 한쪽의 주기가 다른 쪽 수명을 망치니, 정리 대상마다 effect 를 나눠 두는 편이 안전합니다.
댓글 0