타이머 id를 state에 담았다가 불필요한 리렌더링이 계속 일어난 이유
문제 발생
디바운스 타이머 id를 useState에 담았더니 타이핑할 때마다 컴포넌트가 두 번씩 렌더링됐습니다. 그 값은 화면에 전혀 쓰이지 않는데도요.
const [timerId, setTimerId] = useState(null);
function handleChange(value) {
clearTimeout(timerId);
setTimerId(setTimeout(() => search(value), 300)); // 매번 리렌더
}원인 분석
useState는 화면에 반영되어야 하는 값을 위한 것입니다. 값이 바뀌면 React가 리렌더링해서 새 값을 화면에 그립니다. 타이머 id는 화면에 그려지지 않으므로, 그 리렌더링은 전부 낭비입니다.
useRef는 렌더링에 필요 없는 값을 담습니다. 공식 문서의 표현대로 ref를 바꿔도 리렌더링이 일어나지 않습니다. 그래서 렌더 사이에 유지되어야 하지만 화면과 무관한 값에 적합합니다 — 타이머·인터벌 id, 이전 값 기록, 외부 라이브러리 인스턴스 같은 것들입니다.
대신 규칙이 하나 있습니다. 공식 문서가 굵게 강조합니다 — 렌더링 중에는 ref.current를 읽지도 쓰지도 마십시오. React는 컴포넌트 본문이 순수 함수처럼 동작하기를 기대하는데, 렌더 중에 ref를 건드리면 그 전제가 깨집니다.
해결 방안
- 화면과 무관한 값은 ref에 담습니다.
const timerRef = useRef(null);
function handleChange(value) {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => search(value), 300); // 리렌더 없음
}- 읽고 쓰는 위치는 이벤트 핸들러나 effect 안입니다.
function MyComponent() {
myRef.current = 123; // 하면 안 됨 (렌더 중 쓰기)
return <h1>{otherRef.current}</h1>; // 하면 안 됨 (렌더 중 읽기)
}useEffect(() => { myRef.current = 123; }); // 괜찮음
function handleClick() { doSomething(otherRef.current); } // 괜찮음- 화면에 보여야 하면 state입니다. 공식 문서의 기준이 명확합니다 — 렌더링 중에 읽거나 써야 하는 값이라면 state를 씁니다. ref에 담고 화면에 그리면 값이 바뀌어도 갱신되지 않습니다.
- 정리도 잊지 않습니다. 타이머를 ref에 담았다면 언마운트 시 정리해야 합니다.
useEffect(() => () => clearTimeout(timerRef.current), []);- DOM 참조와 같은 훅이지만 목적이 다릅니다.
useRef는 "렌더 사이에 유지되는 상자"이고, DOM 노드를 담는 것은 그 용례 중 하나일 뿐입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.