본문 바로가기
Language/React

[React 공식 Advanced Doc] Forwarding Refs

by ocwokocw 2021. 2. 11.

- 이 글은 React 공식 홈페이지 Docs https://facebook.github.io/react-native/ 에 기반하여 작성한 글입니다

- Forwarding Refs

Ref forwarding은 component에서 그의 자식들에게 ref를 자동으로 전파하는 기법이다. 일반적인 Component에서 사용할일은 없겠지만 특정 상황에서는 유용하게 사용된다. 특히 재사용가능한 component 라이브러리같은 경우가 그렇다.

ref를 사용하는 일반적인 시나리오를 살펴보자.


- Forwarding refs to DOM components

DOM의 button을 rendering 하는 FancyButton component가 있다고 가정해보자.

 

function FancyButton(props) {
	return (
	  <button className="FancyButton">
		{props.children}
	  </button>
	);
  }

ReactDOM.render(
	<FancyButton>
		fancy button
	</FancyButton>, 
	document.getElementById('root')
);

 

React component는 상세적으로 어떻게 구현했는지 그리고 Rendering 을 어떻게 했는가를 숨기고 있다. (예제에서 <FancyButton> 이라는 태그만 보고서 어떻게 그려질지 상세적으로 알 수 없다.) 일반적인 경우에는 FancyButton 내부의 button DOM 요소를 참조할 경우는 거의 없다. 이런 특성은 Component가 DOM 구조에 과도하게 의존하는 것을 막아준다는 점에서 장점이 된다.

 

이런 은닉화가 FeedStory나 Comment같은 어플리케이션 레벨의 Component에서는 바람직하지만, FancyButton이나 MyTextInput과 같이 재사용빈도가 높은 "leaf" component 에서는 불편한 경우가 있을 수도 있다.

 

이런 Component들은 application에서 정규 DOM(button, input)와 비슷하게 다루어지는 경우가 많은데, 해당 Component에서 DOM 노드의 focus나 selection, animations 같은 이벤트를 제어할수도 있다.

 

Ref forwarding은 어떤 Component가 ref 변수명으로 참조를 받아서(C로 치면 포인터를 받아서), 그 참조를 자식으로 전파("forward")시킨다.

 

말이 어려울 수도 있다. 위의 말이 React 에서 어떻게 코드로 표현되는지 예제를 살펴보자. 아래 예제에서 FancyButton은 React.forwardRef를 사용했는데 ref 변수를 전달받아서, rendering 요소중에 DOM button 요소에 포워딩한다.

 

const FancyButton = React.forwardRef((props, ref) => {
	return (
		<button ref={ref} className="FancyButton">
			{props.children}
		</button>
	);
});

const ref = React.createRef();

ReactDOM.render(
	<FancyButton ref={ref}>
		fancy button
	</FancyButton>, 
	document.getElementById('root')
);

 

이런 방법으로 FancyButton을 사용하는 component들은 FancyButton 안에 있는 button DOM 노드를 참조할 수 있다. (DOM button에 직접 사용하는것처럼 사용하면 된다.)

 

꽤 많은 과정이 있었다. 무슨일이 있었는지 살펴보자.

  1. 처음에 React.createRef를 이용하여 React 참조를 생성하고, ref 변수에 할당하였다.
  2. 1.에서 할당한 ref를 <FancyButton ref={ref}> 와 같이 JSX의 attribute로 전달하였다.
  3. React.forwardRef 함수의 2번째 인자로 ref를 전달하였다.
  4. 이 ref 인자를 <button ref={ref}>로 JSX attribute로 결정하였다.
  5. ref.current가 <button> DOM 노드를 가리키도록 하였다.

주의: 두번째 ref 인자는 React.forwardRef를 호출하여 component를 정의했을때만 존재한다. 일반적인 function이나 class component 는 ref를 인자로 받을 수 없고, props에서 ref를 사용하는것도 불가능하다. 또, Ref forwarding을 반드시 DOM component 만 사용해야 한다는 제약사항이 없다. class component의 instance도 참조할 수 있다.


- Note for component library maintainers

component 라이브러리에서 forwardRef를 도입하려고 한다면, breaking change(SW의 한 부분을 바꾸면 다른 component가 동작하지 않는 현상.)를 검토한 후 새로운 major 버전을 배포해야 한다.

 

라이브러리가 다르게 동작하면(어떤 ref가 할당되었는지, 어떤 type이 export 되는지), app들이 동작하지 않을수도 있고, 다른 라이브러리들이 원래 동작하던 현재 라이브러리의 동작에 의존적일수도 있기 때문이다.

 

경우에 따라서 React.forwardRef의 사용이 권장되지 않는 경우가 있다. ref forwarding 때문에 라이브러리 동작이 변경될 수도 있고, React가 업그레이드 되면서 app이 동작하지 않을 수도 있다.


- Forwarding refs in higher-order components

이번 테크닉은 higher-order components(HOCs)와 함께 사용할때 유용할 수 있다. 

우선 props를 console 로그고 남기는 HOC component를 에제를 살펴보자.

 

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

 

"logProps" HOC 는 자신을 감싸고 있는 Component에게 모든 props을 넘긴다. 그리고 결과적으로 rendering되는 결과는 변화가 없다. 이 HOC를 이용하여 "fancy button" component가 넘겨받은 모든 props의 로그를 출력할 수 있다.

 

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);

 

위 예제에서 한 가지 주의할 점이 있다. refs는 전달되지 않는다. 왜냐하면 ref는 prop이 아니기 때문이다. 일종의 key와 같이 React에 의해 특별하게 다루어지기 때문이다.

 

만약 ref를 HOC에 추가하고 싶다면 ref는 감싸진 component(Fancy Button)가 아니라 가장 바깥의 component(LogProps)를 참조해야 한다.

 

위의 말이 헷갈릴수도 있다. ref로 원래 참조하려고 생각했던 FancyButton component가 아니라 실제로는 LogProps를 참조하게 된다.

 

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

 

다행히 React.forwardRef API를 이용해서 명시적으로 FancyButton 내부를 참조할 수 있다. React.forwardRef는 props와 ref를 전달받고 React 노드를 반환하는 render 함수를 사용한다.

 

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

- Displaying a custom name in DevTools

React.forwardRef는 render 함수를 인자로 받는다. React 개발자 도구는 이 함수를 ref를 어떤 fowarding하는 component를 보여줄지를 결정하는데 사용한다.

 

예를들면 아래 component는 개발자도구에서 "ForwardRef"로 표시될 것이다.

 

const WrappedComponent = React.forwardRef((props, ref) => {
  return <LogProps {...props} forwardedRef={ref} />;
});

 

만약 render 함수에 이름을 정의하면 개발자 도구는 해당 이름을 포함해서 표시한다.("ForwardRef(myFunction)")

 

const WrappedComponent = React.forwardRef(
  function myFunction(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
);

 

또 function의 displayName 속성을 이용해서도 이름을 정할 수 있다.

 

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // Give this component a more helpful display name in DevTools.
  // e.g. "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}

 

댓글