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 가 forwardRef 은퇴시켰어. Ref 가 이제 평범한 prop. 9년 살아남은 래퍼가 드디어 불필요해졌어.

뭐가 바뀐 거

React 19 전엔 함수 컴포넌트가 ref 직접 못 받았어 — ref 는 특별했음, React 의 reconciler 가 DOM element + 클래스 인스턴스에 붙임. 부모가 본인 내부 DOM node 에 ref 잡게 하려면 함수를 forwardRef(function MyInput(props, ref) { ... }) 로 감싸야 했어. 래퍼가 두 번째 인자를 ref 로 만든다.

React 19 가 ref 를 일반 prop 으로 만들었어. 타입 잡고, destructure 하고, forward 하고. forwardRef 헬퍼는 (호환성으로) 여전히 동작 — 다만 새 코드는 손대지 마.

Ref 가 중요한 세 자리

  1. DOM node 접근 — input focus, element 측정, scroll into view.
  2. Imperative API — element 메서드 호출 요구하는 서드파티 라이브러리.
  3. Mutable 값 저장useRef({ current: ... }) 를 render-stable mutable box 로. Track 3 lesson 3 에서 다룸.

이 레슨은 첫 둘 — 컴포넌트 경계를 넘나드는 ref.

새 모양

Ref prop 이 React.Ref<T> 운반 (T 가 내부 element 타입). 대부분 그냥 내부 element 에 spread 하거나 assign 하면 끝. Imperative 메서드 노출 필요하면 (다이얼로그의 open(), 복잡한 input 의 focus()) ref 와 함께 useImperativeHandle 사용.

forwardRef 마이그레이션

forwardRef 쓰는 코드베이스 유지보수 중이면 모두 한 번에 마이그레이션할 필요 없어. 둘 다 동작. 새로 쓰는 코드는 prop 직접 — 옛 코드는 다른 이유로 건드릴 때까지 그대로.

Ref 는 탈출구. Ref 손이 가는 첫 본능은 '선언적 방식 없나?' — 자주 있어, controlled input, CSS 로 scroll 위치, focus 관리에 ARIA 속성. 진짜 없을 때 (서드파티 imperative API, 네이티브 DOM 측정) 가 ref 의 자리. 다만 먼저 가지 마.

Code

Before / after — 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 로 감싼 컴포넌트를 React 19 ref-as-prop 스타일로 변환. 원시 <dialog> element 대신 open() + close() 노출하는 useImperativeHandle 가진 Modal 컴포넌트. 부모가 DOM 안 건드리고 modalRef.current?.open() 호출 가능한지 확인. 보너스: useImperativeHandle 제거하고 dialog ref 그대로 통과시켜 봐 — 부모가 보는 API 표면이 얼마나 늘어나는지 관찰.
Hint
네이티브 <dialog>showModal()close() 메서드 가짐. Imperative handle 이 그것들 wrap 해서 호출자가 다른 dialog 메서드 우연히 호출 못 하게 함.

Progress

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

댓글 0

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

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