본문 바로가기
C.W.K.
Stream
Lesson 03 of 07 · published

useRef의 세 가지 쓰임

~14 min · useref, ref, mutable, imperative

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
useRef는 DOM 접근, 렌더링 사이의 값 보존, 명령형 API 연결에 쓰여. 세 용도를 섞지 않도록 각각의 모양을 익혀.

DOM 엘리먼트에 접근하기

useRef로 만든 객체를 JSX의 ref에 넘기면 마운트 뒤 .current가 실제 DOM node를 가리켜. 이벤트 핸들러나 effect에서 focus, 측정, scroll 같은 DOM 메서드를 사용할 수 있어.

화면을 다시 그리지 않고 값을 보존하기

타이머 ID, 이전 prop, WebSocket 연결처럼 렌더링 사이에는 남아야 하지만 바뀌어도 화면을 다시 그릴 필요가 없는 값은 ref에 둘 수 있어. .current를 직접 읽고 쓰며 setter는 없어. 화면에 보여 줄 값은 state에 두고 ref와 구분해.

Callback ref로 연결과 해제를 감지하기

DOM node가 붙고 떨어지는 순간에 작업해야 한다면 객체 대신 함수를 ref로 넘겨. React는 연결할 때 node를, 해제할 때 null을 전달해. IntersectionObserver, ResizeObserver, 서드파티 라이브러리를 안전하게 연결할 때 유용해.

렌더링 중에는 함부로 바꾸지 마

Ref 변경은 React에 알려지지 않으므로 렌더링 결과를 결정하는 값으로 쓰면 화면과 실제 값이 어긋날 수 있어. 이벤트와 effect에서 읽고 쓰는 값에 사용해.

State는 화면을 바꾸고 ref는 화면 밖의 값을 기억해. 값이 달라졌을 때 사용자가 보는 UI도 달라져야 한다면 state가 맞아.

이전 값을 기억하는 데도 쓸 수 있어

렌더링 뒤 effect에서 현재 prop을 ref에 저장하면 다음 렌더링에서 이전 값과 비교할 수 있어. 이 비교가 화면을 바꾸면 state가 필요하지만, 로그나 외부 API 동기화 판단에만 쓰인다면 ref로 충분해.

타이머와 연결 객체는 cleanup과 짝지어

Timeout ID나 WebSocket을 ref에 보관하면 새 작업을 시작하기 전에 이전 것을 취소할 수 있어. 컴포넌트가 사라질 때도 남은 타이머와 연결을 정리해 메모리 누수와 늦은 callback을 막아.

Ref 값을 바꿔도 렌더링은 일어나지 않아. 그 값이 화면에 보여야 한다면 state를 쓰고, 렌더링과 무관한 명령형 handle이나 외부 연결만 ref에 남겨.

Code

용도 1: mount할 때 input에 focus하기·tsx
import { useEffect, useRef } from "react";

function AutoFocusInput() {
  const ref = useRef<HTMLInputElement>(null);
  useEffect(() => {
    ref.current?.focus();
  }, []);
  return <input ref={ref} placeholder="Start typing" />;
}
용도 2: 렌더링과 무관한 변경 가능한 값 보관하기·tsx
import { useEffect, useRef, useState } from "react";

// 이전 값을 기억하는 대표적인 커스텀 훅 패턴이야.
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const previousCount = usePrevious(count);
  return (
    <div>
      <p>Now: {count}, before: {previousCount ?? "never"}</p>
      <button onClick={() => setCount((n) => n + 1)}>Increment</button>
    </div>
  );
}
용도 3: IntersectionObserver를 연결하는 callback ref·tsx
import { useCallback } from "react";

function LazySection({ children }: { children: React.ReactNode }) {
  // useCallback은 callback ref의 참조를 렌더링 사이에 유지해.
  const setRef = useCallback((node: HTMLDivElement | null) => {
    if (!node) return; // null이면 node에서 분리된 상태라 연결할 일이 없어.
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) console.log("In view");
    });
    observer.observe(node);
    // 실제 앱에서는 observer를 해제하는 cleanup도 필요해.
  }, []);

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

External links

Exercise

LazySection의 callback ref가 node에 연결될 때 IntersectionObserver를 만들고 분리될 때 disconnect하도록 고쳐. Observer 객체는 별도 useRef에 저장하고, 섹션을 여러 번 mount와 unmount해 이전 observer가 남지 않는지 확인해. Ref 값을 바꿔도 렌더링 횟수는 늘지 않아야 하며, 화면에 보여야 하는 값은 state로 옮겨야 한다는 차이도 기록해.
Hint
Observer용 useRef<IntersectionObserver | null>와 callback ref의 책임을 나눠. Attach, detach, 다시 attach 세 단계에서 observe와 disconnect 호출 횟수를 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.