본문으로 건너뛰기
개발 머꼬
개발 노트React
hohyeon.dev17

타이머 id를 state에 담았다가 불필요한 리렌더링이 계속 일어난 이유

  • #Engineering Note
  • #React

문제 발생

디바운스 타이머 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를 건드리면 그 전제가 깨집니다.

해결 방안

  1. 화면과 무관한 값은 ref에 담습니다.
const timerRef = useRef(null);

function handleChange(value) {
  clearTimeout(timerRef.current);
  timerRef.current = setTimeout(() => search(value), 300);   // 리렌더 없음
}
  1. 읽고 쓰는 위치는 이벤트 핸들러나 effect 안입니다.
function MyComponent() {
  myRef.current = 123;                 // 하면 안 됨 (렌더 중 쓰기)
  return <h1>{otherRef.current}</h1>;  // 하면 안 됨 (렌더 중 읽기)
}
useEffect(() => { myRef.current = 123; });   // 괜찮음
function handleClick() { doSomething(otherRef.current); }   // 괜찮음
  1. 화면에 보여야 하면 state입니다. 공식 문서의 기준이 명확합니다 — 렌더링 중에 읽거나 써야 하는 값이라면 state를 씁니다. ref에 담고 화면에 그리면 값이 바뀌어도 갱신되지 않습니다.
  2. 정리도 잊지 않습니다. 타이머를 ref에 담았다면 언마운트 시 정리해야 합니다.
useEffect(() => () => clearTimeout(timerRef.current), []);
  1. DOM 참조와 같은 훅이지만 목적이 다릅니다. useRef는 "렌더 사이에 유지되는 상자"이고, DOM 노드를 담는 것은 그 용례 중 하나일 뿐입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.