RSS듀오랩스
React

React useRef와 state의 차이

작성자
듀오랩스 대표·8분 읽기

검색어를 입력하고 1초 동안 멈추면 검색하는 입력칸입니다.

function SearchBox({ onSearch }) {
  let timer = null;

  function handleChange(e) {
    clearTimeout(timer);
    const value = e.target.value;
    timer = setTimeout(() => onSearch(value), 1000);
  }

  return <input onChange={handleChange} />;
}

단독으로 두면 잘 동작합니다. 그런데 입력할 때마다 부모가 검색어를 state 로 받아 다시 렌더링하는 화면에 넣으면, 「노트북」을 한 번에 쳤는데 검색 요청이 글자 수만큼 나갑니다. clearTimeout 이 앞의 타이머를 하나도 지우지 못합니다.

ref 는 DOM 요소를 잡는 도구라는 생각

useRef 를 처음 만나는 곳은 대개 입력칸에 포커스를 주는 코드입니다. const inputRef = useRef(null) 을 만들고 <input ref={inputRef} /> 에 붙인 뒤 inputRef.current.focus() 를 부릅니다. 그래서 ref 를 「JSX 요소를 자바스크립트에서 잡기 위한 도구」로 기억하게 됩니다.

이 이해로는 앞의 타이머 문제에 ref 를 떠올리기 어렵습니다. 타이머 id 는 DOM 요소가 아니기 때문입니다. 실제로 ref 는 DOM 과 상관없는 더 일반적인 도구이고, DOM 을 담는 것은 그 쓰임새 중 하나입니다.

렌더링마다 사라지는 지역 변수

먼저 타이머가 왜 안 지워졌는지 보겠습니다. 리렌더링 글에서 렌더링은 React 가 컴포넌트 함수를 호출하는 일이라고 했습니다. 함수가 다시 호출되면 let timer = null 도 다시 실행됩니다.

첫 글자를 치면 이번 렌더링의 timer 에 타이머 id 가 들어갑니다. 부모가 다시 렌더링하면서 SearchBox 도 다시 호출되고, 새 렌더링의 timer 는 다시 null 입니다. 두 번째 글자의 clearTimeout(timer)null 을 지우려 합니다. 첫 타이머는 누구도 기억하지 않은 채 1초 뒤에 실행됩니다.

렌더링 사이에 값을 기억하는 방법으로 state 를 떠올릴 수 있습니다. 하지만 타이머 id 를 state 에 넣으면 id 를 저장할 때마다 다시 렌더링이 일어납니다. 화면에 보이지도 않는 값 때문에 렌더링이 늘어납니다.

기억은 하지만 렌더링을 일으키지 않는 상자

Referencing Values with Refs 문서는 ref 를 이렇게 소개합니다.

When you want a component to "remember" some information, but you don't want that information to trigger new renders, you can use a ref.

ref 는 current 라는 속성 하나를 가진 평범한 자바스크립트 객체입니다. useRef 문서에 따르면 React 는 첫 렌더링에서 이 객체를 만들고, 다음 렌더링부터는 같은 객체를 돌려줍니다. current 를 바꿔도 React 는 알지 못하므로 렌더링이 일어나지 않습니다.

타이머 코드를 ref 로 고치면 이렇습니다.

function SearchBox({ onSearch }) {
  const timerRef = useRef(null);

  function handleChange(e) {
    clearTimeout(timerRef.current);
    const value = e.target.value;
    timerRef.current = setTimeout(() => onSearch(value), 1000);
  }

  return <input onChange={handleChange} />;
}

렌더링이 몇 번 일어나도 timerRef 는 같은 상자이고, 상자 안의 타이머 id 는 살아남습니다.

state 와 ref 를 가르는 기준

두 도구의 차이는 표 하나로 정리됩니다.

state ref
렌더링 사이에 값이 남는가 남는다 남는다
바꾸면 다시 렌더링되는가 된다 안 된다
바꾸는 방법 set 함수 (다음 렌더링에 반영) current 에 바로 대입
렌더링 중에 읽어도 되는가 된다 안 된다 (초기화 제외)

고르는 질문은 하나입니다. 이 값이 바뀌면 화면이 달라져야 하는가. 그렇다면 state, 아니라면 ref 입니다. 타이머 id, 이전 값과 비교하기 위해 저장한 값, 외부 라이브러리 인스턴스, 요청을 취소하기 위한 AbortController 가 ref 에 어울리는 값입니다.

렌더링 중에는 current 를 읽지도 쓰지도 않는다

표의 마지막 줄이 가장 자주 어겨집니다. useRef 문서의 문장입니다.

Do not write or read ref.current during rendering, except for initialization. This makes your component's behavior unpredictable.

렌더링 중에 ref.current 로 화면을 그리면 문제가 두 가지 생깁니다. current 가 바뀌어도 렌더링이 일어나지 않으니 화면이 따라오지 않습니다. 그리고 state 스냅샷 글에서 본 「렌더링마다 고정된 사진」이라는 성질이 ref 에는 없습니다. ref 는 렌더링 사이에서 공유되는 하나의 상자라, 언제 읽느냐에 따라 값이 다릅니다. 렌더링은 같은 입력에 같은 결과를 내야 하는데, 그 약속이 깨집니다.

그래서 ref 를 읽고 쓰는 자리는 이벤트 핸들러와 효과 안입니다. 앞의 타이머 코드가 handleChange 안에서만 timerRef 를 다루는 이유입니다. 문서가 말하는 「초기화 예외」는 비어 있을 때만 한 번 채우는 경우입니다.

const playerRef = useRef(null);
if (playerRef.current === null) {
  playerRef.current = new VideoPlayer();
}

DOM 을 담는 것도 같은 상자

이제 처음의 포커스 예로 돌아가면 새롭게 보입니다. <input ref={inputRef} /> 는 React 에게 「이 DOM 요소가 만들어지면 inputRef.current 에 넣어 달라」고 부탁하는 것입니다. React 는 DOM 을 만든 뒤(커밋 단계) current 를 채우고, 요소가 사라지면 null 로 되돌립니다.

렌더링 중에 inputRef.current.focus() 를 부르면 아직 DOM 이 없을 수 있습니다. 그래서 DOM ref 도 이벤트 핸들러나 효과 안에서 씁니다. DOM ref 가 특별한 것이 아니라, 「렌더링에 쓰지 않는 값을 담는 상자」에 React 가 DOM 요소를 넣어 줄 뿐입니다.

ref 를 자주 쓰게 된다면

문서는 ref 를 「탈출구(escape hatch)」라고 부르며, 자주 필요하지 않다고 적습니다. 저는 ref 가 많아지는 컴포넌트를 한 번 의심해 보는 편이 좋다고 봅니다. 화면에 영향을 주는 값을 ref 에 넣고 강제로 다시 렌더링하는 코드, DOM 을 직접 고쳐 React 가 그린 내용과 어긋나게 만드는 코드가 ref 를 통해 들어오기 쉽기 때문입니다. ref 에 담는 값이 「화면과 무관한가」를 매번 확인하면 대부분 걸러집니다.

여기까지가 확실한 부분

ref 가 렌더링을 일으키지 않고 기억만 한다는 점, 같은 객체를 돌려준다는 점, 렌더링 중에 읽고 쓰지 말라는 규칙과 초기화 예외, 탈출구라는 표현은 react.dev 문서 기준입니다. 첫 예시에서 타이머가 지워지지 않는 동작은 렌더링이 함수 호출이라는 문서의 정의에서 따라 나오는 결과이고, 부모가 입력마다 다시 렌더링하는 구조를 가정했습니다.

이 게시글 공유하기

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.