본문 바로가기

javascript/react

[React] useImperativeHandle을 타입스크립트와 사용하는 방법

useImperativeHandle을 이용하면 하위 컴포넌트의 변수나 함수를 상위 컴포넌트에 공개하여 사용할 수 있다. 이게 단순한 자바스크립트 기반으로 코드를 작성하는 경우에는 별로 어렵지 않은데, 타입스크립트 기반으로 작성하기 시작하면 꽤 까다롭다고 느끼게 된다. 인터넷에 검색해도 영어 문서밖에 안나와서 이번 기회로 사용법을 정리하고자 한다.

다음 문서를 참조했다.

 

declare type with React.useImperativeHandle()

function App(){ const cntEl:any = React.useRef(null); // I don't know what type should be here. React.useEffect(()=>{ if(cntEl.current){ cuntEl.current.start() } }, []); return <

stackoverflow.com

 

forwardRef 의 사용

forwardRef는 ForwardRefRenderFunction을 입력 받아, ForwardRefExoticComponent 의 객체를 반환하는 함수이다. 해당 함수의 결과로 컴포넌트는 forwardRef을 이용한다는 정보를 가진 래퍼 객체로 포장되고, 리액트는 해당 래퍼 객체가 가진 타입 정보를 이용하여 일반적인 컴포넌트와는 다른 동작을 수행할 수 있다. 

이때 forwardRef의 파라미터인 render의 타입 ForwardRefRenderFunction의 구조는 다음과 같다.

react.createRef로 넘겨주게 되는 함수 컴포넌트는 ForwardRefRenderFunction을 구현한다. 즉, forwardRef을 이용할 때 함수 컴포넌트의 타입은 ForwardRenderFunction이 된다.

ForwardedRef 타입의 구현은 위와 같은데, 실질적으로는 상위에서 전달되는 ref에 대한 타입 정보를 저장한다.

이런 정보들을 감안하면, forwardRef의 파라미터로 전달될 함수 컴포넌트는 ForwardRefRenderFunction을 구현해야 하며, 해당 인터페이스의 제네릭 인자로는 <Ref의 타입, props의 타입> 정보가 전달되어야 한다. 이를 정리하면 다음과 같은 코드가 된다.

const Input: React.ForwardRefRenderFunction<HTMLInputElement, props>
    = (props: props, ref) => {
    //some codes...
    return (
    	<input
            ref={ref}>
        );
    };
    
export default React.forwardRef(Input);

forwardRef가 가리키는 대상이 실제 DOM에 대응되는 엘리먼트라면, 해당 엘리먼트를 가리키는 인터페이스를 제네릭의 ref 인자로 넘겨주면 된다. 위 예시에서는 forwardRef을 통해 input 엘리먼트의 ref를 포워딩하려고 했으므로, ForwardRefRenderFunction 제네릭의 첫번째 인자로 input 엘리먼트를 가리키는 HTMLInputElement를 전달했다.

이후 해당 Ref을 상위 컴포넌트에서 사용하기 위해서는 다음과 같이 코드를 작성하게 된다.

const idRef = useRef<HTMLInputElement>(null);

useRef의 제네릭 인자로 input 엘리먼트를 가리키는 HTMLInputElement를 전달했다. 즉, 상위 컴포넌트의 useRef 제네릭 인자로는 ref 변수로 가리키고 싶은 함수 컴포넌트에서의 ref 타입을 전달하면 된다.

 

useImperativeHandle 의 사용

만약 useImperativeHandle을 사용하는 경우는 어떨까? useImperativeHandle 을 통해 전달할 ref가 가리키는 것은 실제로 DOM 상에 인터페이스로 정의된 엘리먼트를 가리키는 것이 아니다. 해당 hook이 하위 컴포넌트의 특정 변수 및 함수만을 상위로 공개한다는 것을 감안하면 ForwardRefRenderFunctiond의 제네릭 인자인 ref로 전달되어야 하는 인터페이스는 해당 컴포넌트에서 공개하고 싶은 특정 변수 및 함수 정보를 가진 인터페이스가 될 것이다. 이를 고려하여 다음 예제를 한번 살펴보도록 하자.

//... some codes

type props = Partial<{
    id: string,
    label: string,
    type: htmlInputType,
    onBlur: () => void,
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
    value: string
}>;
// props의 타입 정보

export type inputHandle = {
     focus: () => void,
    };
// ref로 전달할 인터페이스 정보

const Input: React.ForwardRefRenderFunction<inputHandle, props>
    = (props: props, ref) => {

    const inputRef = useRef<HTMLInputElement>(null);
    
    const focus = () => {
        inputRef.current!.focus();
    };

    useImperativeHandle(ref, () => {
        return {
            focus: focus
        };
    });
    // 위에서 정의한 inputHandle 인터페이스와 동일한 구조의 객체를 반환해야 함.

    return (
        <div className={styles.line}>
            <label
                className={styles.label}
                htmlFor={props.id}
            >
                {props.label}
            </label>
            <input
                className={styles.input}
                ref={inputRef}
                id={props.id}
                name={props.id}
                type={props.type}
                onBlur={props.onBlur}
                onChange={props.onChange}
                value={props.value}
            />
        </div>
    )
};
 
export default React.forwardRef(Input);

위 예제에서는 useImperativeHandle에서 { focus : () => void } 를 반환하고 있다. 이때 ForwardRefRenderFunction의 제네릭 인자 ref에는 inputHandle이 오고 있는데, 우리가 반환하고 있는 객체는 해당 타입에 속한다. (타입스크립트에서 type과 interface는 유사한 지위를 지닌다 )

상위 컴포넌트에서는 다음과 같이 ref의 제네릭 인자를 지정한다.

const idRef = useRef<inputHandle>(null); 
// 인터페이스를 직접 전달
const passwordRef = useRef<React.ElementRef<typeof Input>>(null); 
// 인터페이스를 ElementRef을 이용해 전달

제네릭 인자 ref에는 하위 컴포넌트에서 ref로 전달했던 타입을 그대로 전달하거나, ElementRef<typeof Component> 형태로 전달할 수 있다. ElementRef을 이용하면 해당 컴포넌트의 Ref 타입 정보를 반환하는데, forwardRef를 거친 컴포넌트의 Ref 타입은 ForwardRefRenderFunction의 제네릭 인자 ref에 지정한 타입과 동일하다.

ForwardRefRenderFunction 숨기기

forwardRef는 ForwardRefRenderFunction을 인자로 받는다. 이때 자바스크립트 기반으로 코드를 작성했던 기억을 떠올려보자. 자바스크립트 기반에서 forwardRef를 이용하는 경우, 다음과 같이 코드를 작성할 수 있었다.

const Component = React.forwardRef((props,ref) => {
	//... codes
};

export default Component;

 

위와 같은 방식은 타입스크립트 기반으로도 작동하며, ForwardRefRenderFunction을 따로 명시하지 않아도 되기 때문에 더 간결하게 느껴진다.

const Input= React.forwardRef<inputHandle, props>((props: props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);
    // some codes ...
});
 
export default Input;

ForwardRefRenderFunction에서 사용했던 제네릭 인자들을 그대로 forwardRef에 붙인 후, 함수 컴포넌트를 forwardRef의 실인자로 넘겨주면 ForwardRefRenderFunction을 굳이 고려하지 않고 코드를 작성할 수 있다. 단, 해당 타입을 명시하는 것과 export할 때 forwardRef을 하는 것 등의 과정은 온전히 자신의 선택이므로, 자신이 원하는 방식을 이용하자. 어차피 결과적으로는 동일하기 때문에 큰 상관도 없다.

결론

forwardRef 및 useImperativeHandle을 이용하는 경우, ref에 전달하고자 하는 인터페이스 정보를 제공해야 한다. 이후 상위 컴포넌트에서 해당 인터페이스를 useRef hook의 제네릭 인자로 넘겨 하위 컴포넌트에 대한 ref를 연결할 수 있다.

 

사용된 예시 코드는 아래 주소에서 확인할 수 있다.

 

GitHub - blaxsior/JS_Study: 자바스크립트와 관련된 공부 내용을 모아 둔 레포지토리

자바스크립트와 관련된 공부 내용을 모아 둔 레포지토리. Contribute to blaxsior/JS_Study development by creating an account on GitHub.

github.com

useImperativeHandle이 사용된 컴포넌트는 Input.tsx 파일이고, 해당 ref는 Login.tsx 파일에서 사용되었다.