학습 자료

useRef — 기억하되 다시 그리지 않는다


Article

8강에서 값을 기억하는 두 가지 방법이 실패했다. 함수 안의 변수는 초기화되고, 함수 밖의 변수는 공유된다.

그래서 useState를 썼다. 그런데 useState바뀌면 화면을 다시 그린다.

화면과 무관한 값을 기억해야 한다면 어떻게 할까.

다시 그릴 필요가 없는 값

타이머 ID를 저장한다고 하자.

function Timer() {
  const [intervalId, setIntervalId] = useState(null);

  function start() {
    const id = setInterval(tick, 1000);
    setIntervalId(id);
  }

  function stop() {
    clearInterval(intervalId);
  }
}

동작은 한다. 그런데 setIntervalId를 부르는 순간 컴포넌트가 다시 그려진다. 화면에 아무 변화도 없는데 헛수고다.

useRef를 쓴다.

import { useRef } from "react";

function Timer() {
  const intervalRef = useRef(null);

  function start() {
    intervalRef.current = setInterval(tick, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
  }
}

useRef{ current: 값 } 형태의 상자 하나를 돌려준다. 그 상자는 렌더링이 반복돼도 계속 같은 상자다.

useRef 가 돌려주는 것

렌더링이 몇 번 일어나도 같은 객체를 돌려준다

intervalRef
0x1a2b
{ current: 42 }

state 와 ref

같은 "기억하기"인데 결정적으로 다르다.

useState 와 useRef

useState

화면에 보이는 값

  • 바뀌면 다시 그린다
  • 직접 수정 금지 (12강)
  • setState 로만 바꾼다
  • 그 렌더링의 스냅샷이다

useRef

화면과 무관한 값

  • 바뀌어도 아무 일 없다
  • ref.current = x 로 직접 수정
  • 언제든 읽고 쓴다
  • 항상 최신값을 준다

판단 기준은 하나다. 이 값이 바뀌면 화면이 바뀌어야 하는가.

무엇으로
입력창의 글자state화면에 보여야 한다
타이머 IDref화면과 무관하다
이전 렌더의 값ref비교용일 뿐이다
DOM 요소ref화면을 그리는 재료가 아니다
클릭 횟수 (표시함)state보여줘야 한다
클릭 횟수 (기록만)ref안 보여준다

렌더 중에 읽고 쓰지 않는다

13강에서 렌더 함수는 순수해야 한다고 했다. ref.current를 렌더 중에 건드리면 그 규칙이 깨진다.

function Bad() {
  const ref = useRef(0);
  ref.current++;
  return <p>{ref.current}</p>;
}

StrictMode에서 두 번 실행되면 값이 두 번 오른다. 순수하지 않다.

이벤트 핸들러나 useEffect 안에서 읽고 쓴다.

DOM 을 직접 다룬다

useRef의 두 번째 쓰임이자 더 흔한 쓰임이다.

React는 DOM을 대신 관리해준다. 하지만 직접 만져야 할 때가 있다.

function SearchInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

ref 속성에 넘기면 React가 커밋 단계에서 실제 DOM 노드current에 넣어준다.

DOM 노드가 ref 에 담기는 시점
  1. 렌더current 는 아직 null
  2. 커밋DOM 이 만들어진다
  3. ref.current = 노드React 가 넣어준다
  4. useEffect이제 쓸 수 있다

렌더 중에는 null이다. 15강에서 본 실행 순서를 떠올리면 당연하다. DOM이 아직 없는데 담아줄 수가 없다.

언제 DOM 을 직접 만지나

React가 못 하는 일에만 쓴다.

써도 되는 일쓰면 안 되는 일
포커스 주기 (focus)텍스트 바꾸기
스크롤 위치 조정요소 추가 · 삭제
요소 크기 재기클래스 붙이기
미디어 재생 (play · pause)스타일 바꾸기
외부 라이브러리에 노드 넘기기화면에 보이는 것 조작

오른쪽 것들은 전부 state로 해결된다. 1강의 선언형을 버리는 순간 데이터와 화면이 어긋나기 시작한다.

function Bad({ isActive }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isActive) ref.current.classList.add("active");
    else ref.current.classList.remove("active");
  }, [isActive]);

  return <div ref={ref}></div>;
}

function Good({ isActive }) {
  return <div className={isActive ? "active" : ""}></div>;
}

아래가 짧고, 안전하고, React의 방식이다.

자식의 DOM 을 받는다

부모가 자식 안의 input에 포커스를 주고 싶다면 ref를 넘긴다.

function TextInput({ ref }) {
  return <input ref={ref} />;
}

function Form() {
  const inputRef = useRef(null);

  return (
    <>
      <TextInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>입력창으로</button>
    </>
  );
}

React 19부터 ref를 그냥 props처럼 받을 수 있다. 그 전에는 forwardRef로 감싸야 했다. 옛 코드에서 forwardRef를 보면 이 이야기라고 생각하면 된다.

이전 값을 기억한다

ref가 리렌더링을 일으키지 않는다는 성질을 이용한 패턴이다.

function Counter({ count }) {
  const prevCount = useRef(count);

  useEffect(() => {
    prevCount.current = count;
  }, [count]);

  return (
    <p>
      {count} (이전: {prevCount.current})
    </p>
  );
}

렌더링 중에는 아직 useEffect가 안 돌았으므로 prevCount.current이전 값이 남아 있다. effect가 실행되면서 새 값으로 갱신된다.

정리하면

  • useRef렌더링을 일으키지 않고 값을 기억한다. { current: 값 } 상자를 준다.
  • 그 상자는 렌더링이 반복돼도 같은 상자다.
  • 판단 기준은 하나다. 바뀌면 화면이 바뀌어야 하는가. 그렇다면 state, 아니면 ref.
  • ref를 화면에 그리지 않는다. 값이 바뀌어도 화면이 안 바뀐다.
  • 렌더 중에 ref.current를 읽거나 쓰지 않는다. 순수성이 깨지고, DOM ref는 아직 null이다.
  • DOM은 React가 못 하는 일에만 직접 만진다. 포커스·스크롤·크기 측정 정도다.
  • 텍스트·클래스·스타일은 state로 한다. 직접 만지면 1강의 문제가 돌아온다.

다음 강의에서는 여러 컴포넌트가 같은 값을 나눠 쓰는 법, 상태 끌어올리기와 Context를 본다. Part 5의 마지막이다.