본문 바로가기
Frontend2026년 8월 21일7분 읽기

React 19 ref prop — forwardRef 없이 ref 전달하기

YS
김영삼
조회 3
React 19 ref prop — forwardRef 없이 ref 전달하기

React 19부터는 함수 컴포넌트가 ref일반 prop처럼 그냥 받을 수 있다. 즉 forwardRef로 감싸지 않아도 function Input({ ref }) { ... }처럼 매개변수에서 바로 꺼내 쓸 수 있고, forwardRef 자체는 deprecated 되어 미래 버전에서 제거될 예정이다.

ref를 컴포넌트 밖으로 노출하려고 forwardRef로 감싸던 그 성가신 래핑이 드디어 사라졌다. 개인적으로는 디자인 시스템에서 Input, Button 같은 저수준 컴포넌트를 만들 때마다 forwardRef를 습관처럼 붙였는데, 이제 그럴 필요가 없어져서 코드가 눈에 띄게 깔끔해졌다.

예전 방식과 새 방식

React 18까지는 이랬다. ref를 받으려면 무조건 forwardRef로 감싸고, 콜백의 두 번째 인자로 ref가 넘어왔다.

// React 18 이하
import { forwardRef } from "react";

const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

React 19에서는 ref가 그냥 props의 한 필드다.

// React 19
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// 사용 측은 그대로
<Input ref={inputRef} placeholder="이름" />

호출하는 쪽 코드는 하나도 안 바뀐다. 바뀌는 건 컴포넌트를 정의하는 쪽뿐이라, 마이그레이션 부담이 생각보다 적다.

ref 콜백에 클린업이 생겼다

같이 알아두면 좋은 변화가 하나 더 있다. ref를 콜백으로 넘길 때, 이제 클린업 함수를 반환할 수 있다. DOM이 붙을 때 실행되고, 떨어질 때 반환한 함수가 호출된다. 예전에는 언마운트 시 콜백이 null로 다시 불리는 걸로 정리했는데, 그 패턴이 명시적으로 바뀐 셈이다.

<input
  ref={(node) => {
    if (!node) return;
    const onFocus = () => console.log("focus");
    node.addEventListener("focus", onFocus);
    // 클린업 반환
    return () => node.removeEventListener("focus", onFocus);
  }}
/>

주의할 점: 클린업을 반환하기 시작하면 React는 더 이상 언마운트 시 콜백을 null로 호출하지 않는다. 그래서 예전 습관대로 if (node === null) 분기로 정리 로직을 넣어두면 그게 영영 안 불릴 수 있다. 둘을 섞지 말자.

정리표

상황React 18React 19
ref 전달forwardRef 래핑ref를 prop으로 직접
ref 콜백 정리node===null 분기클린업 함수 반환
forwardRef필수deprecated

마이그레이션은 codemod로

기존 프로젝트에 forwardRef가 수십 개라면 손으로 다 고칠 필요는 없다. React 팀이 제공하는 codemod를 돌리면 대부분 자동 변환된다.

npx codemod@latest react/19/replace-reactdom-render
# forwardRef 제거용
npx types-react-codemod@latest ...

다만 자동 변환이 항상 완벽하진 않다. 제네릭 컴포넌트나 조건부로 ref를 다루던 코드는 타입이 꼬일 수 있으니, 변환 후 타입 체크는 꼭 돌려보자. 나는 한 번 제네릭 리스트 컴포넌트에서 Ref<T> 타입이 안 맞아 손으로 고쳤다.

디자인 시스템에서 특히 체감된다

저수준 컴포넌트를 많이 만드는 디자인 시스템 팀이라면 이 변화의 이득이 가장 크다. Button, Input, Textarea, Checkbox 같은 프리미티브는 거의 예외 없이 외부에서 DOM ref에 접근할 일이 있어서, 예전엔 전부 forwardRef로 감싸야 했다. 컴포넌트가 서른 개면 서른 개 전부 래핑 코드가 붙었고, 그 안에서 다시 제네릭이나 다형성(as prop)을 얹으면 타입이 급격히 지저분해졌다.

ref가 그냥 prop이 되면서 이 계층이 얇아진다. 폴리모픽 컴포넌트를 만들 때도 ref를 props 타입에 자연스럽게 녹일 수 있어, forwardRef의 제네릭 시그니처와 씨름하던 시간이 사라졌다. 실제로 우리 팀 컴포넌트 라이브러리를 옮겨보니 파일당 두세 줄씩 줄었고, 무엇보다 타입 힌트가 훨씬 읽기 편해졌다.

한 가지 덧붙이면, forwardRef를 쓸 때 습관적으로 붙이던 displayName 지정도 이제는 함수 선언에 이름만 제대로 주면 DevTools에 그대로 뜬다. 익명 화살표 함수로 넘겨서 컴포넌트 이름이 "Anonymous"로 보이던 문제도 자연스럽게 정리된다.

자주 묻는 질문

forwardRef를 지금 당장 다 걷어내야 하나요?

아닙니다. React 19에서 forwardRef는 여전히 동작하며 deprecated 경고만 뜹니다. 급하게 전부 바꿀 필요는 없고, 새로 만드는 컴포넌트부터 ref를 prop으로 받는 방식을 쓰면서 점진적으로 옮기면 됩니다. 실제 제거는 이후 메이저 버전에서 이루어질 예정입니다.

클래스 컴포넌트도 ref를 prop으로 받나요?

아니요. 이 변경은 함수 컴포넌트에 한정됩니다. 클래스 컴포넌트의 인스턴스 ref는 기존과 동일하게 동작하며, ref를 prop처럼 받는 문법은 적용되지 않습니다.

ref라는 이름의 prop을 다른 용도로 쓰고 있었다면요?

충돌합니다. React 19에서 ref는 예약된 특수 prop이 되므로, 데이터 전달용으로 ref라는 이름을 쓰고 있었다면 다른 이름으로 바꿔야 합니다. 마이그레이션 전에 프로젝트에서 ref prop 사용처를 검색해 확인하는 것을 권합니다.

TypeScript에서 ref 타입은 어떻게 지정하나요?

props 타입에 ref 필드를 직접 넣으면 됩니다. 예를 들어 input이라면 ref의 타입을 Ref<HTMLInputElement>로 선언하고 나머지 props와 함께 구조 분해하면 됩니다. forwardRef의 제네릭 인자를 기억할 필요가 없어서 오히려 타입 쓰기가 직관적입니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록