useImperativeHandle
부모 컴포넌트에서 자식 컴포넌트 HTML 요소를 제어하려면 ref를 prop(forwardRef)으로 자식 컴포넌트에게 넘겨주어야 합니다.
type MyInputProps = ComponentPropsWithoutRef<'input'>;
type MyInputRef = HTMLInputElement;
const MyInput = forwardRef<MyInputRef, MyInputProps>(
function MyInput(props, ref) {
return <input css={{ border: '1px solid black' }} {...props} ref={ref} />;
},
);
export default function ParentComponent() {
const ref = useRef<HTMLInputElement>(null);
const handleClick = () => {
console.log(ref.current); // <input/>
};
return (
<div>
<div
css={{ display: 'flex', flexDirection: 'column', maxWidth: '400px' }}
>
<MyInput ref={ref} />
<button css={{ border: '1px solid black' }} onClick={handleClick}>
focus button
</button>
</div>
</div>
);
}
기본적인 사용 방법
부모 컴포넌트에서 버튼을 클릭하면 handleClick 이벤트 함수가 실행되어 콘솔 로그에는 <input/>
요소가 찍히게 됩니다.
해당 요소 대신 사용자 지정 메서드를 노출시켜 로직을 커스텀할 수 있는데, 이때 사용하는 훅이 바로 useImperativeHandle 입니다. 자식 요소에서 useImperativeHandle를 사용하여 전달받은 ref를 첫 번째 인자로 넣으면 버튼을 클릭했을 때 이전과 다르게 빈 객체가 찍히게 됩니다.
const MyInput = forwardRef<MyInputRef, MyInputProps>(
function MyInput(props, ref) {
useImperativeHandle(ref, () => {
return {};
}, []);
// ...
},
);
export default function ParentComponent() {
const ref = useRef<HTMLInputElement>(null);
const handleClick = () => {
console.log(ref.current); // {}
};
// ...
}
이제 useImperativeHandle 활용하여 focus 메서드 만을 노출시키고자 합니다. MyInputRef의 타입을 다음과 같이 변경하여 자식과 부모 모두 ref 타입을 변경해 주었습니다.
type MyInputRef = {
focus: () => void;
};
const MyInput = forwardRef<MyInputRef, MyInputProps>(function MyInput(
// ...
))
export default function ParentComponent() {
const ref = useRef<MyInputRef>(null);
// ...
}
ref 참조는 다음과 같이 focus만을 추론하게 됩니다.
다음으로는 자식 컴포넌트에서 useImperativeHandle 콜백 함수의 반환 값을 수정하고, ref를 새로 추가하여 참조하고자 하는 dom에 연결해 주어야 합니다.
const MyInput = forwardRef<MyInputRef, MyInputProps>(
function MyInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current?.focus();
},
};
}, []);
return (
<input css={{ border: '1px solid black' }} {...props} ref={inputRef} />
);
},
);
이제 input 요소의 권한이 없어지고, focus 메서드를 호출할 수 있습니다.
그렇다면 useImperativeHandle 언제 활용할 수 있을까요?
부모가 자식 컴포넌트의 두 개 이상의 DOM을 조작하는 경우 유용하게 사용할 수 있습니다. focusAndScroll와 같이 메서드 이름은 DOM 메서드와 일치할 필요가 없습니다. 또한 자식 컴포넌트는 내부 상태나 로직을 관리하면서 부모와 결합을 낮출 수 있습니다.
type MyInputProps = ComponentPropsWithoutRef<'input'>;
type MyInputRef = {
focusAndScroll: () => void;
};
const MyInput = forwardRef<MyInputRef, MyInputProps>(
function MyInput(props, ref) {
const divRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => {
return {
focusAndScroll() {
inputRef.current?.focus();
const node = divRef.current;
if (node) {
node.scrollTop = node?.scrollHeight;
}
},
};
}, []);
return (
<>
<div css={{ overflowY: 'scroll', height: '100px' }} ref={divRef}>
{Array(18)
.fill('Comment')
.map((content, i) => (
<div key={content}>comment {i}</div>
))}
</div>
<input css={{ border: '1px solid black' }} {...props} ref={inputRef} />
</>
);
},
);
export default function ParentComponent() {
const ref = useRef<MyInputRef>(null);
const handleClick = () => {
ref.current?.focusAndScroll();
};
return (
<div>
<div
css={{ display: 'flex', flexDirection: 'column', maxWidth: '400px' }}
>
<MyInput ref={ref} />
<button css={{ border: '1px solid black' }} onClick={handleClick}>
focus button
</button>
</div>
</div>
);
}
ref를 과도하게 사용하지 마세요.
공식 문서에서는 ref를 props로 표현할 수 없는 필수적인 행동(특정 노드로 스크롤 하기, 노드에 초점 맞추기, 애니메이션 촉발하기, 텍스트 선택하기 등)에서만 사용해야 한다고 합니다.