React는 많은 hook을 제공한다. 그중 유효하지만 평소 잘 쓰지 않는 것 중에 useImperativeHandle이 있다.
이름마저 생소한 이 hook의 기능은 생각보다 강력하다.
forwarding된 ref를 replace 할 수 있는 기능을 제공하기 때문에 forwardRef와 같이 쓴다.
ForwardRef
forwardRef는 말그대로 reference를 전달해주는 기능을 한다. 이 reference는 DOMElement일수도 있고 ReactComponentInstance일수도 있다.
내부에 있는 DOM에 접근할 경우는 크게 문제가 없지만 만약 child component 내부에 있는 DOD에 접근을 한다면 우리는 이를 간단하게 forwardRef를 통해 넘겨줄수 있다.
const FormControl = React.forwardRef((props, ref) => {
return (
<div className="form-control">
<input type="text" ref={ref} />
<button
onClick={() => {
ref.current.focus();
}}>
focus
</button>
</div>
);
})const App = () => {
const inputRef = React.useRef();
return (
<div className="App">
<FormControl ref={inputRef} />
</div>
);
}
App은 Input에 inputRef를 넘김으로 해서 자식의 ref를 받아서 아주 쉽게 처리된다.
하지만 위의 코드는 커다란 문제를 안고 있다.
ref를 참조하는 FormControl 입장에서는 부모가 자신에게 ref Object를 넘겼는지 알수가 없기때문이다. (ref를 외부에서참조하는 케이스가 많지 않아서 문제가 쉽게 접하지 않는 문제이다.) 만약 깜박하고 App에서 ref를 넘기지 않았다면 버튼을 클릭 했을 때 input이 있더라도 ref 자체가 null로 넘어와 에러가 일어나게 된다.
그렇다면 부모가 ref를 넘겼을때는 ref로 아니면 자신이 스스로 ref를 만들어 생성해야하므로 우리는 다음과 같은 코드를 생각해 낼 수 있다.
const FormControl = React.forwardRef((props, ref) => {
// store Refconst tempRef = useRef();
const [inputRef, setInputRef] = useState(ref || tempRef);
return (
<div className="form-control">
<input type="text" ref={inputRef} />
<button
onClick={() => {
inputRef.current.focus();
}}>
focus
</button>
</div>
);
})
useImperativeHandle
그렇다면 이 같은 경우를 useImperativeHandle 를 쓴다면 어떻게 될까?
아래는 그 예시이다.
const FormControl = React.forwardRef((props, ref) => {
const inputRef = useRef(null);
// useImperativeHandle 은 reference를 맵핑한다.
useImperativeHandle(ref, () => inputRef.current); return (
<div className="form-control">
<input type="text" ref={inputRef} />
<button
onClick={() => {
inputRef.current.focus();
}}>
focus
</button>
</div>
);
})
위의 useState를 썼을때보다 코드가 간단해지거나 더 쓰기 편해진 것은 아니다. 그렇다면 우리가 useImperativeHandle를 이용해서 얻을 수 있는 이점이 더 있을까?
코드를 자세히보면 우리가 넣은 2개의 argument 에서 처음은 부모가 보낸 reference, 그리고 두번째는 자식이 맞춰 보낼 reference로 이는 부모가 reference.current로 접속할 수 있게 맵핑을 해주고 있다.
즉 부모에게 꼭 자식의 실제 reference를 보내지 않고 우리가 원하는 일종의 proxy reference를 보내는게 가능해진다는 것이다.
이는 함수형 컴포넌트에서 가질 수 없는 내부 method를 외부에 보낼수 있다는 것을 의미하며 또한 내부 자식을 private으로 그대로 남겨둘 수 있다는 것이기도 하다.
const FormControl = React.forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({getValue(){
return inputRef.current.value
}) }, [inputRef]); // deps도 추가가능하다. return (
<div className="form-control">
<input type="text" ref={inputRef} />
</div>
);
})const App = () => {
const inputRef = React.useRef();
return (
<div className="App">
<FormControl ref={inputRef} />
<button
onClick={() => {
console.log(inputRef.current.getValue());
}}>
focus
</button>
</div>
);
}
위에서보면 부모는 자식의 DOM에 직접적으로 접근을 하는 것이 아니라 useImperativeHandle로 전달된 메서드에만 접근이 가능해진다. 이로서 더욱 컴포넌트간의 독립성을 보장할 수가 있게 되는 것이다.
React 공식 홈페이지에서는 아래와 같이 설명하고 있다.
useImperativeHandle
customizes the instance value that is exposed to parent components when usingref
. As always, imperative code using refs should be avoided in most cases.useImperativeHandle
should be used with forwardRef.
Ref를 이용하는 자체가 권장되는 사용은 아니지만 혹시나 라이브러리를 wrapping 할때, 혹은 부모에게 자식의 메서드를 넘겨야하는 상황이 발생 할때 useImperativeHandle 는 유용하게 사용할 곳이 많을 것 같다.