본문 바로가기

javascript/react

[React] forwardRef & useImperativeHandle

useRef는 실제 DOM 상의 엘리먼트를 참조하고, 이들을 직접 제어하기 위한 hook이다. 이때 해당 hook은 기본적으로 리액트에서 제공하는, html의 각 태그에 대응되는 기본 JSX에 대해 작동한다. 여기서 고민이 있다. 기본 제공되는 JSX는 ref로 접근할 수 있는데, 그렇다면 우리가 만드는 컴포넌트는 어떻게 ref로 접근할 수 있을까?

 위의 질문을 다시 한번 조정해보자. 커스텀 컴포넌트를 ref로 접근한다는 의미가 무엇일까? 예를 들어 input JSX을 내부적으로 가지고 있는 Input 커스텀 컴포넌트을 ref로 접근하고 싶다는 것은 Input 커스텀 컴포넌트 내부의 input JSX에 접근하고 싶다는 의미로 받아들여질 수 있다. 이 경우 커스텀 컴포넌트는 일종의 래퍼이고, 실제로 접근하고 싶은 것은 기본적으로 제공되는 JSX이다. 다른 경우도 있다. 특정 컴포넌트에 접근하여 특정한 값만을 ref로 가져오고 싶은 경우, ref로 접근하고 싶은 대상은 해당 컴포넌트 내부의 특정 값이다. ( 콜백 방식과는 달리 특정 이벤트 없이도 사용 가능 )

 즉 ref로 커스텀 컴포넌트를 접근하고 싶다는 것은 1, 해당 커스텀 컴포넌트 내부의 기본 JSX에 대한 ref에 접근하는 것과 2. 해당 컴포넌트 내부에서 관리하는 특정 state나 함수 등에 접근하고 싶다 라는 의미로 해석될 수 있다. 문제는 useRef만으로는 두 경우 모두 충족할 수 없다는 것이다.

 위와 같은 상황에서 커스텀 컴포넌트에 대한 ref을 제공하기 위해 forwardRef 및 useImperativeHandle가 존재한다.

forwardRef

 forwardRef는 함수 컴포넌트 내부를 ref 기반으로 접근하기 위해 필요한 함수로, 함수 컴포넌트를 입력받고 ForwardRefExoticComponent을 반환한다. ForwardRefExoticComponent에 대한 자세한 설명은 얻을 수 없으나, 대신 해당 인터페이스의 상속 관계를 거슬러 올라가면 등장하는 ExoticComponent의 동작을 통해 유추할 수 있다.

 ExoticComponent는 일종의 래퍼 객체의 역할을 수행하는데 해당 객체로 래핑되는 컴포넌트들은 다른 컴포넌트들과 다른 방식으로 렌더링 될 수 있으며, 해당 인터페이스가 가지는 $$typeof 변수는 props으로 가지고 있는 엘리먼트가 어떤 종류인지에 대한 정보를 담고 있다. 이런 동작을 감안하면 ForwardRefExoticComponent는 forwardRef 을 적용한 컴포넌트에 대해 부여되는 래퍼 객체이며, 대상이 되는 컴포넌트는 렌더링 되는 방식이 일반적인 컴포넌트와 다를 수 있다는 것을 알 수 있다.

ExoticComponent의 정의

 

forwardRef 의 사용 방법은 다음과 같다.

 

const Component = (props, forwardRef) => {
	// some codes;
    
    return (
    <input
        ...
        ref={forwardRef}/>
};

export default React.forwardRef(Component);

forwardRef을 적용할 컴포넌트는 외부에서 연결한 ref ( ref = { someRef } 형태로 연결되는 reference ) 을 가리키는 forwardRef 파라미터를 추가로 가진다. 해당 파라미터를 연결할 JSX 엘리먼트의 ref로 전달하면 외부에서도 해당 JSX 엘리먼트에 접근할 수 있다.

 

useImperativeHandle

상위 컴포넌트에서 하위 컴포넌트의 특정한 값 ( 변수 혹은 함수 등 ) 을 ref로 접근하기 위해서는 useImperativeHandle이 필요하다. 

const Component = (props, ref) => {
    //some codes
    
    useImperativeHandle(ref, () => {
        return {
            arg1: value1,
            arg2: value2,
            arg3: func1,
            ...
        };
    });
    
    return (<some-components>
        ...
    </some-components>);
}

export default React.forwardRef(Component);

useImperativeHandle은 2개의 파라미터를 가진다. 

  • ref : 상위에서 전달되는 ref 을 가리키는 파라미터
  • init : 상위에 전달할 값을 지정하고, 이를 반환하는 함수

위와 같이 지정하면, 상위 컴포넌트에서 직접 해당 컴포넌트의 변수나 함수에 접근할 수 있다.

 함수를 전달하는 경우 추가적으로 고려해야 하는 점이 있다. 함수의 구현 방식에 따라 this를 이용하여 해당 함수가 실행된 특정 환경의 값들에 기반하는 경우가 있는데, 무작정 해당 함수를 복사하는 경우 this가 가리키는 대상이 하위 컴포넌트의 환경이 아니라 상위 컴포넌트의 환경을 가리키게 되어 이상한 대상에 대해 함수를 실행할 수도 있기 때문에 가능하면 하위 컴포넌트에서 해당 함수를 실행하기 위한 래퍼 함수를 만들고, 이를 init을 통해 상위 컴포넌트에 공개하는 것이 이상한 오류를 막는데 도움이 될 수 있다.

오류 예시

 

 

 예를 들어보자. 위 그림 내의 코드는 input 엘리먼트에 대해 focus를 나타낸다. inputRef는 현재 컴포넌트 내부의 input 엘리먼트를 가리키는데, focus라는 함수는 자신이 속한 환경에 대해 동작한다. 이때 위처럼 래퍼 함수로 해당 함수를 감싸서 전달하는 경우, inputRef.current!.focus은 input 엘리먼트라는 렉시컬 환경에서 수행되기 때문에 문제가 발생하지 않는다.

 그런데, 다음과 같이 함수를 전달하는 것은 문제를 발생시킨다.

 

위 코드에서는 inputRef.current!.focus 자체를 전달한다. 위에서 언급했듯이 focus 함수는 해당 함수가 포함된 환경에서 동작하는데, 이 함수 자체를 반환하여 상위 컴포넌트에서 실행하면 focus의 환경은 상위 컴포넌트 기준으로 정해지기 때문에 최소한 하위 컴포넌트의 inputRef를 가리키지는 않게 된다.

illegal invocation 에러가 발생한 모습. focus가 자신이 실행된 환경에서 수행될 수 없기 때문에 에러를 발생시킨다.

 

요약 및 결론

forwardRef 및 useImperativeHandle은 상위 컴포넌트에서 하위 컴포넌트의 정보를 ref을 이용하여 접근하고 싶을 때 사용한다. useImperativeHandle은 특히 하위 컴포넌트의 특정 변수나 함수를 콜백 형식을 이용하지 않고 사용하려고 할 때 유용할 수 있다. 이때 useImperativeHandle을 통해 함수를 직접 전달하는 경우 함수에 this를 이용한 구현이 있다면, 상위 컴포넌트에서 해당 함수를 사용할 때 상위 컴포넌트의 렉시컬 환경을 참조하게 될 수도 있기 때문에 래퍼 함수를 이용하여 전달하는 것이 안전하다.