forwardRef 는 컴포넌트가 ref와 함께 DOM 노드를 상위 컴포넌트에 노출할 수 있게 합니다.
const SomeComponent = forwardRef(render)
forwardRef() 를 호출하여 컴포넌트가 ref를 받고, 해당 ref를 자식 컴포넌트로 전달할 수 있게 합니다.
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
// ...
});
render: 컴포넌트의 렌더 함수입니다. React는 이 함수를 부모로부터 받은 props와 ref와 함께 호출합니다. 반환하는 JSX는 컴포넌트의 출력이 됩니다.forwardRef 는 JSX에서 렌더링할 수 있는 React 컴포넌트를 반환합니다. 일반 함수로 정의된 React 컴포넌트와는 달리, forwardRef 에서 반환된 컴포넌트는 ref 프롭을 받을 수 있습니다.
forwardRef는 렌더 함수를 인수로 받습니다. React는 이 함수를 props와 ref와 함께 호출합니다.
const MyInput = forwardRef(function MyInput(props, ref) {
return (
<label>
{props.label}
<input ref={ref} />
</label>
);
});
props: 부모 컴포넌트에서 전달된 props입니다.ref: 부모 컴포넌트에서 전달된 ref 속성입니다. ref 는 객체나 함수일 수 있습니다. 부모 컴포넌트에서 ref를 전달하지 않은 경우, ref는 null일 것입니다. 전달받은 ref 를 다른 컴포넌트에 전달하거나 useImperativeHandle 에 전달해야 합니다.forwardRef 는 JSX에서 렌더링할 수 있는 React 컴포넌트를 반환합니다. 일반 함수로 정의된 React 컴포넌트와는 달리, forwardRef 에서 반환된 컴포넌트는 ref 프롭을 전달받을 수 있습니다.
기본적으로 각 컴포넌트의 DOM 노드는 개인적입니다. 그러나 때로는 상위 컴포넌트에 DOM 노드를 노출하는 것이 유용할 수 있습니다. 예를 들어, 포커싱을 가능하게 하기 위해서입니다. 이를 위해 컴포넌트 정의를 forwardRef()로 래핑하세요.
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<input {...otherProps} />
</label>
);
});
props 이후 두 번째 인수로 ref를 받게 됩니다. 노출하려는 DOM 노드에 해당 ref를 전달하세요.
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<input {...otherProps} ref={ref} />
</label>
);
});
이를 통해 부모 Form 컴포넌트에서 MyInput에 의해 노출된 <input> DOM 노드에 접근할 수 있습니다.3
function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
}
return (
<form>
<MyInput label="Enter your name:" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
이 Form 컴포넌트는 MyInput에 ref를 전달합니다. MyInput 컴포넌트는 해당 ref를 브라우저의 <input> 태그로 전달합니다. 이 결과로, Form 컴포넌트는 해당 <input> DOM 노드에 접근하고 focus()를 호출할 수 있습니다.
컴포넌트 내부의 DOM 노드에 ref를 노출하면 나중에 컴포넌트의 내부를 변경하기가 더 어려워집니다. 일반적으로 버튼이나 텍스트 입력 같은 재사용 가능한 하위 컴포넌트에서 DOM 노드를 노출하지만, 아바타나 댓글 같은 응용 프로그램 수준의 컴포넌트에서는 그렇게 하지 않습니다.
DOM 노드로 ref를 전달하는 대신, MyInput과 같은 컴포넌트로 ref를 전달할 수 있습니다.
const FormField = forwardRef(function FormField(props, ref) {
// ...
return (
<>
<MyInput ref={ref} />
...
</>
);
});
만약 MyInput 컴포넌트가 그것의 <input>으로 ref를 전달한다면, FormField의 ref는 그 <input>을 제공합니다.
function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
}
return (
<form>
<FormField label="Enter your name:" ref={ref} isRequired={true} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
Form 컴포넌트는 ref를 정의하고 FormField에 전달합니다. FormField 컴포넌트는 이 ref를 MyInput에 전달하고, MyInput은 브라우저의 <input> DOM 노드로 전달합니다. 이렇게 Form은 그 DOM 노드에 접근합니다.
전체 DOM 노드를 노출하는 대신, 더 제한된 메서드 집합을 가진 사용자 정의 객체인 imperative handle을 노출할 수 있습니다. 이를 위해서는, DOM 노드를 보유할 별도의 ref를 정의해야 합니다.
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
// ...
return <input {...props} ref={inputRef} />;
});
전달받은 ref를 useImperativeHandle에 전달하고, ref에 노출할 값을 지정합니다.
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
만약 어떤 컴포넌트가 MyInput의 ref를 받는다면, 해당 DOM 노드 대신 { focus, scrollIntoView } 객체만 받게 될 것입니다. 이렇게 함으로써 노출하는 DOM 노드에 대한 정보를 최소한으로 제한할 수 있습니다.
❗️ Pitfall
ref를 과도하게 사용하지 마십시오. ref는 프롭으로 표현할 수 없는 명령형 동작에만 사용해야 합니다. 예를 들어 노드로 스크롤링, 노드에 포커스, 애니메이션 트리거, 텍스트 선택 등입니다.
만약 프롭으로 표현할 수 있다면, ref를 사용해서는 안 됩니다. 예를 들어Modal컴포넌트에서{ open, close }와 같은 명령형 핸들을 노출하는 대신<Modal isOpen={isOpen}/>과 같이isOpen을 프롭으로 사용하는 것이 더 좋습니다. 효과(Effects)를 사용하면 명령형 동작을 프롭을 통해 노출할 수 있습니다.