Udemy React 강의 137번 정리
이 강의에서는 ref의 두 번째 핵심 용도를 다룬다. DOM 요소 말고도 UI에 직접 영향 없는 값을 저장할 때 ref를 쓸 수 있다는 점.
특히 타이머 예제를 통해 왜 변수나 useState로는 안 되고, 왜 ref가 정답인지 설명한다.
setTimeout이 반환하는 타이머는 아래와 같이 사용할 수 있다.
const timer = setTimeout(() => {
setTimerExpired(true)
}, targetTime * 1000)
clearTimeout(timer)
즉, 타이머를 멈추려면 타이머 ID(pointer)가 필요함.
그래서 처음엔 이렇게 생각할 수 있다.
let timer // 타이머 ID 저장
function handleStart() {
timer = setTimeout(...)
}
function handleStop() {
clearTimeout(timer)
}
하지만 이렇게 하면 제대로 작동하지 않는다.
문제를 일으키는 핵심 이유 2가지 :
React는 state가 바뀌면 컴포넌트를 재실행(re-run)한다.
그리고 컴포넌트 안의 let timer는 렌더링이 다시 일어날 때마다 새로 선언되고, 이전 값은 사라진다.
즉, handleStart에 저장한 타이머 ID가 handleStop 실행 시점에는 사라져 버린 상태가 된다.
그래서 clearTimeout()이 제대로 호출되지 않음.
이 문제는 더 심각하다.
강의 예제처럼 5초 타이머 / 1초 타이머 두 개가 따로 렌더링된다면 어떻게 될까?
let timer
이 변수가 컴포넌트 함수 밖에 선언된다면, 두 컴포넌트 인스턴스가 같은 timer 변수를 공유하게 된다.
결과는,
1. 5초 타이머 시작 → timer = 5초 타이머 ID
2. 1초 타이머 시작 → timer = 1초 타이머 ID로 덮어씀
3. 5초 타이머 stop 누르면? → 이미 ID가 덮었음 → 5초 타이머는 안 멈춰짐
4. 결국 You lost! 뜸
즉, 컴포넌트 별로 독립된 타이머가 필요한데, 변수는 전부 공유된다.
state를 사용하면, 또 다른 문제가 발생한다.
state로 타이머 ID를 저장하면,
const [timerId, setTimerId] = useState()
문제는,
즉, UI를 바꿀 필요 없는 값 때문에 리렌더를 유발하면 불필요한 성능 소모가 발생한다.
ref는 다음과 같은 특징을 가지기 때문.
ref.current는 컴포넌트가 여러 번 렌더링되어도 값이 보존됨.
즉, handleStart에서 저장한 ID → handleStop에서도 그대로 살아 있음.
각 TimerChallenge 컴포넌트마다 샌드박스처럼 독립된 ref 생성됨.
즉, 5초/1초 타이머가 서로 타이머 ID를 덮어쓰지 않음.
state와 반대로 ref.current 변경은 리렌더를 유발하지 않고, UI에 영향을 주지 않는 값 관리에 최적함.
코드로 보자.
const timer = useRef();
function handleStart() {
timer.current = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
}
function handleStop() {
clearTimeout(timer.current);
}
timer.current 값 유지그래서 타이머 ID 같은 값은 ref로 관리하는 것이 정답이다.