Menu

React forwardRef: ref 전달과 React 19의 ref prop

forwardRef를 쓰면 컴포넌트가 부모에게서 ref를 받아 안쪽의 DOM 노드에 붙일 수 있습니다. React 19에서는 함수 컴포넌트가 ref를 일반 prop으로 받으므로 새 코드에는 forwardRef가 더 이상 필요 없습니다. 두 버전을 실행해 보고 useImperativeHandle과 TypeScript 타입도 살펴봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

forwardRef를 쓰면 부모 컴포넌트가 직접 만든 컴포넌트를 거쳐 그 안의 DOM 요소까지 ref를 넘길 수 있으므로, 부모가 그 요소에 focus(), scrollIntoView() 같은 메서드를 호출할 수 있습니다. React 19에서는 더 이상 필요 없습니다. 함수 컴포넌트가 ref를 일반 prop으로 받기 때문입니다. 그래도 forwardRef는 여전히 동작하고 기존 코드 대부분에서 보게 되므로, 이 페이지에서는 둘 다 보여 줍니다.

forwardRef는 두 인자를 가진 렌더 함수를 받습니다. props와 부모가 넘긴 ref입니다. ref는 props 안에 있지 않고 따로 도착합니다. Focus를 클릭하면 콘솔에서 inputRef.current가 실제 <input> 노드라는 것을 확인할 수 있습니다.

React 19 방식: ref는 prop입니다

React 19부터 함수 컴포넌트는 다른 prop처럼 props에서 ref를 받습니다. 래퍼도, 두 번째 인자도 없습니다.

무언가를 입력하고 Log value를 클릭해 보세요. 부모가 DOM 노드에서 input의 현재 텍스트를 바로 읽습니다. <input>에서 ref={ref}를 지우고 Focus를 클릭하면, inputRef.current가 이제 null이므로 콘솔에 오류가 표시됩니다.

forwardRef(모든 버전)ref prop(React 19+)
컴포넌트가 ref를 받는 방법두 번째 인자, (props, ref)props.ref
래퍼 필요 여부필요불필요
useImperativeHandle과 함께 사용가능가능
상태동작함, 지원 중단 예정새 코드를 작성하는 방식

자식이 무시한 ref는 null로 남습니다

컴포넌트에 ref를 넘기는 것만으로는 아무 일도 일어나지 않습니다. 컴포넌트가 그것을 요소에 붙여야 합니다. 그렇지 않으면 ref.current는 null로 남습니다.

콘솔에는 Forgetful ref: null과 Careful ref: INPUT이 표시됩니다. 직접 만든 컴포넌트에서 "ref.current is null"이 나오는 가장 흔한 이유입니다. React 19 이전에는 ref가 함수에 전달되지 않았으므로, forwardRef로 감싸지 않았다면 Careful도 null이었을 것입니다.

useImperativeHandle: 더 작은 API 노출하기

부모에게 DOM 노드 전체를 주면 부모가 그 노드에 무엇이든 할 수 있습니다. 스타일을 바꾸고, 자식을 제거하고, 비공개로 두려던 값을 읽을 수 있습니다. useImperativeHandle을 쓰면 대신 ref.current에 무엇이 들어갈지 정할 수 있습니다.

마지막 버튼은 ["focus", "clear"]를 기록합니다. 부모는 input 노드가 아니라 그 두 메서드만 받습니다. 객체에 select() 메서드와 그것을 호출하는 버튼을 추가하면 부모는 정확히 한 가지 능력을 더 얻습니다.

자식은 실제 input을 위한 자기만의 ref inputRef를 유지하고, 핸들이 그것을 감쌉니다. 세 번째 인자는 useEffect의 것과 같은 의존성 배열이며, 그 값들이 바뀌면 핸들이 다시 만들어집니다.

이 기능은 아껴서 쓰세요. 부모가 원하는 대부분의 것(열기, 닫기, 오류 표시)은 isOpen이나 error 같은 props로 하는 편이 낫습니다. 명령형 메서드는 자연스러운 prop이 없는 동작을 위한 것입니다. 포커스, 스크롤, 비디오 재생, 애니메이션 시작 같은 것들입니다.

자식 안에서도 ref 사용하기

때로는 부모가 노드에 대한 ref를 가지고 있는 동안, 자식도 측정하거나 오류 후 포커스하는 등 자기 일을 위해 같은 DOM 노드가 필요합니다. ref 속성 하나는 값 하나만 받을 수 있으므로, ref 콜백으로 둘을 합치세요.

function AutoGrowTextarea({ ref, ...props }) {
    const localRef = useRef(null);

    function setRefs(node) {
        localRef.current = node;
        if (typeof ref === 'function') ref(node);
        else if (ref) ref.current = node;
    }

    return <textarea ref={setRefs} {...props} />;
}

부모의 ref는 useRef에서 온 객체일 수도, 함수일 수도 있으므로 둘 다 처리하세요. 부모에게 동작 몇 개만 필요하다면 자식이 노드를 혼자 가지고 있는 위의 useImperativeHandle이 더 깔끔한 선택입니다.

여러 단계를 거쳐 ref 넘기기

ref는 한 번에 컴포넌트 하나씩 이동합니다. Form이 Field를, Field가 TextInput을, TextInput이 <input>을 렌더링한다면, 각 컴포넌트가 다음 컴포넌트로 ref를 넘겨야 합니다. React 19에서는 전달할 prop이 하나 더 있는 것일 뿐이지만(<TextInput ref={ref} />), forwardRef에서는 모든 단계를 감싸야 했습니다. 이전 버전에서는 props 전개({...props})로도 전달되지 않습니다. React 19 이전에는 ref가 props에 포함된 적이 없기 때문입니다.

이것이 보통 중요해지는 곳은 디자인 시스템 컴포넌트입니다. 네이티브 요소를 감싸는 Button, Input, Select는 ref를 전달해야 이를 쓰는 앱이 포커스하거나, 측정하거나, 팝오버를 배치하는 라이브러리에 넘길 수 있습니다.

forwardRef에서 마이그레이션하기

변경은 기계적입니다. 래퍼를 제거하고 props에서 ref를 읽으세요.

// Before
const Button = forwardRef(function Button({ variant, ...props }, ref) {
    return <button ref={ref} className={variant} {...props} />;
});

// After (React 19)
function Button({ variant, ref, ...props }) {
    return <button ref={ref} className={variant} {...props} />;
}

서두를 필요는 없습니다. forwardRef는 React 19에서도 계속 동작하고, React 18은 ref를 prop으로 넘기지 않으므로 React 18을 지원해야 하는 라이브러리는 그대로 유지해야 합니다. 클래스 컴포넌트는 영향을 받지 않습니다. 클래스 컴포넌트의 ref는 여전히 컴포넌트 인스턴스를 가리킵니다.

TypeScript

forwardRef에서는 타입 인자가 ref 타입, 그다음 props 순서로 옵니다.

import { forwardRef } from 'react';

type FancyInputProps = { label: string };

const FancyInput = forwardRef<HTMLInputElement, FancyInputProps>(
    function FancyInput({ label }, ref) {
        return <input ref={ref} aria-label={label} />;
    }
);

React 19에서는 다른 prop처럼 ref의 타입을 지정합니다. ComponentProps<'input'>에는 이미 포함되어 있습니다.

import { useImperativeHandle, useRef, type ComponentProps, type Ref } from 'react';

function FancyInput(props: ComponentProps<'input'>) {
    return <input {...props} />;
}

type SearchHandle = { focus: () => void; clear: () => void };

function SearchBox({ ref }: { ref?: Ref<SearchHandle> }) {
    const inputRef = useRef<HTMLInputElement>(null);
    useImperativeHandle(ref, () => ({
        focus: () => inputRef.current?.focus(),
        clear: () => {
            if (inputRef.current) inputRef.current.value = '';
        },
    }));
    return <input ref={inputRef} />;
}

// In the parent
const searchRef = useRef<SearchHandle>(null);

useRef 페이지에서 ref 자체를 다룹니다. DOM 접근, 렌더링 없이 유지되는 값, ref 콜백입니다. 컴포넌트 타입 지정 전반은 React와 TypeScript를 참고하세요.

자주 묻는 질문

React에서 forwardRef는 무엇을 하나요?

함수 컴포넌트를 감싸서, 부모가 준 ref가 두 번째 인자 (props, ref)로 컴포넌트에 전달되게 합니다. 컴포넌트는 그 ref를 DOM 노드에 붙이므로, 부모가 그 노드에 focus() 같은 메서드를 호출할 수 있습니다.

React 19에서 forwardRef는 지원 중단되었나요?

아직은 아니며 여전히 동작합니다. React 19는 함수 컴포넌트에 ref를 일반 prop으로 넘기므로 새 코드에는 필요 없고, React 팀은 향후 버전에서 forwardRef를 지원 중단할 계획이라고 밝혔습니다.

직접 만든 컴포넌트의 ref가 null인 이유는 무엇인가요?

컴포넌트가 ref를 받았지만 어떤 요소에도 붙이지 않았기 때문입니다. 안쪽의 DOM 노드에 붙이세요: <input ref={ref} />. React 19 이전에는 컴포넌트를 forwardRef로 감싸지 않으면 ref가 아예 전달되지 않았습니다.

useImperativeHandle은 어디에 쓰나요?

부모가 ref.current에서 보는 것을 바꿉니다. DOM 노드 전체 대신 focus, clear처럼 직접 고른 메서드만 담은 객체를 반환합니다.

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

아니요. 클래스 컴포넌트의 ref는 여전히 컴포넌트 인스턴스를 가리킵니다. 이 변경은 함수 컴포넌트에만 적용됩니다.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기