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

React 19에서는 ref도 prop이야

~13 min · react-19, ref, forwardref

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React 19에서는 ref를 평범한 prop처럼 받을 수 있어. 새 코드에서 forwardRef 래퍼가 왜 필요 없어졌는지 확인해.

React 19에서 달라진 점

이전에는 함수 컴포넌트가 ref를 직접 받을 수 없었어. 내부 DOM 엘리먼트에 부모의 ref를 연결하려면 forwardRef로 함수를 감싸고 두 번째 인자로 ref를 받아야 했지. React 19에서는 ref가 일반 prop처럼 전달돼. 타입을 선언하고 구조 분해한 뒤 내부 엘리먼트에 넘기면 돼. 기존 forwardRef 코드는 계속 동작하므로 다른 이유 없이 한꺼번에 바꿀 필요는 없어.

Ref가 필요한 자리

  1. input에 focus를 주거나 크기를 재고 화면으로 scroll하는 등 DOM node에 접근할 때 사용해.
  2. 명령형 메서드를 요구하는 서드파티 라이브러리를 연결할 때 필요해.
  3. 렌더링 사이에 값을 보존하되 변경이 화면을 다시 그릴 필요가 없을 때 useRef를 사용해. 이 용도는 Track 3에서 더 자세히 다뤄.

타입과 명령형 API

Ref prop에는 보통 React.Ref<T>를 사용하고, T에는 내부 DOM 엘리먼트 타입을 넣어. 내부 메서드 전체가 아니라 focus()open()처럼 제한된 API만 공개해야 한다면 useImperativeHandle로 노출할 모양을 정해.

Ref는 선언적인 방법이 없을 때 쓰는 탈출구야. 제어되는 input, CSS, 상태와 prop으로 해결할 수 있는지 먼저 확인해. DOM 측정이나 외부 명령형 API처럼 정말 필요한 경계에서만 사용하면 돼.

Callback ref도 정리 함수를 반환할 수 있어

React 19의 callback ref는 node를 받았을 때 cleanup 함수를 돌려줄 수 있어. Observer나 외부 widget을 연결한 자리에서 바로 해제 로직을 함께 둘 수 있으므로 attach와 detach 계약이 한곳에 남아.

Ref 타입을 목적에 맞게 골라

부모가 넘기는 ref prop에는 React.Ref<HTMLInputElement>처럼 callback과 객체 ref를 모두 받을 수 있는 타입이 알맞아. 컴포넌트 안에서 직접 만든 객체라면 RefObject의 current를 사용해. 여러 내부 node를 통째로 공개하지 말고 부모가 실제로 필요한 엘리먼트나 메서드만 연결해.

Code

변경 전과 후: ref 있는 같은 MyInput·tsx
// BEFORE (React 18 이하 — 호환성으로 19 에서도 동작)
import { forwardRef } from "react";

export const MyInput = forwardRef<HTMLInputElement, { label: string }>(
  function MyInput({ label }, ref) {
    return (
      <label className="flex flex-col gap-1">
        <span>{label}</span>
        <input ref={ref} className="border p-2 rounded" />
      </label>
    );
  }
);

// AFTER (React 19 — ref가 그냥 prop)
type MyInputProps = {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
};

export function MyInput({ label, ref }: MyInputProps) {
  return (
    <label className="flex flex-col gap-1">
      <span>{label}</span>
      <input ref={ref} className="border p-2 rounded" />
    </label>
  );
}

// 호출자 코드는 둘 다 동일:
// const inputRef = useRef<HTMLInputElement>(null);
// <MyInput label="Name" ref={inputRef} />
// inputRef.current?.focus();
useImperativeHandle: 명시적 메서드 노출·tsx
import { useImperativeHandle, useRef } from "react";

type DialogHandle = {
  open: () => void;
  close: () => void;
};

type DialogProps = {
  ref?: React.Ref<DialogHandle>;
  children: React.ReactNode;
};

export function Dialog({ ref, children }: DialogProps) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  // 원시 DOM element 대신 작은 API를 명시적으로 publish.
  useImperativeHandle(
    ref,
    () => ({
      open: () => dialogRef.current?.showModal(),
      close: () => dialogRef.current?.close(),
    }),
    []
  );

  return (
    <dialog ref={dialogRef} className="rounded-card p-6">
      {children}
    </dialog>
  );
}

// 호출자가 DOM element 전혀 안 만짐:
// const dialogRef = useRef<DialogHandle>(null);
// <Dialog ref={dialogRef}>...</Dialog>
// dialogRef.current?.open();

External links

Exercise

forwardRef로 감싼 MyInput을 React 19의 ref prop 방식으로 바꾸고 부모 버튼에서 focus가 동작하는지 확인해. 이어서 네이티브 dialog를 내부에 둔 Modal을 만들고 useImperativeHandle로 open과 close만 공개해. Raw dialog ref를 그대로 넘긴 버전과 비교해 부모가 볼 수 있는 메서드가 얼마나 달라지는지 적고, mount 전 optional chaining도 확인해.
Hint
Dialog의 showModal과 close를 작은 handle로 감싸. 부모가 내부 DOM 구조와 다른 메서드를 몰라도 요구한 두 동작만 호출할 수 있어야 해.

Progress

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

댓글 0

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

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