React ref, useImperativeHandle, forwardRef

2 minute read

얼마전에 ref 를 활용해야하는 일이 생겨 React Hook 을 이용한 Ref 활용법에 대해 기록을 남기고자한다. 우선 Hook는 함수기반 컴포넌트에서 상태를 다를수 있는 기능을 제공하는 도구이다.

함수형 컴포넌트와 Hook의 사용으로 재사용가능한 코드를 만드는데 도움을 주지만 컴포넌트 생명주기 및 ref 사용에 제약이 있다.

이번에 문제가 된 부분은 Navi 영역중 About 버튼을 눌렀을때 Dialog를 출력시키는 기능을 구현하는것이였다.

pic1

pic2

컴포넌트 구성은 아래와 같을때 NaviBar의 About 버튼 클릭으로 Dialog 컴포넌트의 출력 함수를 실행시켜야했다. 즉 부모가 자식의 상태를 변경시켜야하는 문제다.

- NaviBar
    - Dialog

물론 Class 기반 컴포넌트를 사용하고 있다면 Parent쪽에 Child의 상태를 변경하는 함수를 전달하면 쉽게 해결될 문제지만…

현재 사용하고 있는 Material UI 라이브러리는 대부분의 컴포넌트이 Hook을 사용한 함수형 컴포넌트이다. 따라서 상태를 변경하기 쉽지가 않았다.

그런고로 stockoverflow를 검색한 결과 useRef를 이용하기로 하였다.

잠시 ref에 대해 짚고 넘어가자면 ref는 React에서 DOM에 대해 이름을 지정하는 기능이다. 이 ref 명을 통해 어느 DOM이던 컴포넌트던간에 접근이 가능한것이다.

물론 장점이 있다면 단점이 있기 마련이다. 특히 단점이 매우 극명한데 React 공식 문서 및 여러 개발자 블로그에도 언급되듯 아래와 같은 단점이 있다.

  1. 컴포넌트의 캡슐화가 깨진다.
  2. 어디서 접근하는지 알수가 없다.

따라서 공식문서에서는 Ref를 사용할때에는 아래의 경우에만 사용하도록 권장하고 있다.

Ref의 바람직한 사용 사례는 다음과 같습니다.

포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
애니메이션을 직접적으로 실행시킬 때.

서드 파티 DOM 라이브러리를 React와 같이 사용할 때.
선언적으로 해결될 수 있는 문제에서는 ref 사용을 지양하세요.

예를 들어, Dialog 컴포넌트에서 open()과 close() 메서드를 두는 대신, isOpen이라는 prop을 넘겨주세요.


현재 내가 겪고있는 문제가 딱 서드 파티 DOM 라이브러리사용에 해당된다.

이야기가 좀 많이 옆길로 샌것같아 다시 바로잡으면, React Hook을 사용한 컴포넌트에서 Ref를 사용하기 위해서는 아래의 함수가 필요하다.

useRef

const refContainer = useRef(initialValue);
useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지될 것입니다.

좀 쉽게 생각하면 ref 객체를 반환받을수 있는 함수라고 볼수있다. 여기서 반환된 객체는 공식 문서에 나와있는것처럼 컴포넌트의 모든생애 주기를 통해 유지된다.

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화(customizes)합니다. 항상 그렇듯이, 대부분의 경우 ref를 사용한 명령형 코드는 피해야 합니다. useImperativeHandle는 forwardRef와 더불어 사용하세요.

이 함수는 이름부터가 의미가 남다른데 Imperative의 뜻은 피할수 없는, 명령적인 을 가지고 있다. 즉 강제로 사용하겠다는 의미를 가지고 있는것이다.

React.forwardRef

React.forwardRef는 전달받은 ref 어트리뷰트를 하부 트리 내의 다른 컴포넌트로 전달하는 React 컴포넌트를 생성합니다. 이 기법은 잘 사용되지 않지만, 아래의 두 시나리오에서는 특히 유용합니다.
  • DOM 엘리먼트로 ref 전달하기
  • 고차 컴포넌트(Higher Order Component)로 ref 전달하기

위 세개 함수의 역할을 정리하면 다음과 같이 정리할 수 있다.

  • useRef : ref 객체 반환
  • useImperativeHandle : ref 객체를 부모 컴포넌트에 노출
  • React.forwardRef : ref 어트리뷰트를 다른 컴포넌트로 전달.

위 함수를 이용하여 실제 문제를 해결해보았다.


//forwardRef를 통해 전달
const NaviDialogs = forwardRef((props, ref) => {
  const [open, setOpen] = React.useState(false);

  const handleClickOpen = () => {
    setOpen(true);
  };
  const handleClose = () => {
    setOpen(false);
  };

  //https://ko.reactjs.org/docs/hooks-reference.html#useimperativehandle
  //자식의 ref를 부모에게 노출시킴
  //되도록 사용하면 안되는 패턴임
  useImperativeHandle(ref, () => {
    return {
      handleClickOpen: handleClickOpen
    }
 });


return (
        //...
    )
});
//부모 컴포넌트 내부

export default function PersistentDrawerLeft() {

  //ref 객체 생성
  const ref = useRef(null);
 
  //...

  function handleDrawerOpen() {
    setOpen(true);
  }

  function handleDrawerClose() {
    setOpen(false);
  }

  const handleListItemClick = (event, id) => {
 
    scroller.scrollTo(id, {
      duration: 800,
      delay: 0,
      offset : -64,
      smooth: 'easeInOutQuart'
    });
  }

  //ref.current 객체를 통해 자식 컴포넌트의 handleClickOpen()에 접근함
  const handleOpenDialog = () =>{
    ref.current.handleClickOpen();
  }

  }

    return (
        //...
    )

});

마무리

문제는 해결했지만 애초에 공식문서에서 말했듯 Ref는 특수한 경우가 아니면 사용을 자제해야한다. 이번에는 Dialog 컴포넌트가 서드파티 라이브러리를 이용한 컴포넌트라 커스텀이 어려워 Ref를 사용한것이다.(물론 이는 내 실력이 부족하여 그럴수도 있다. 다른방법이 있을수 있다.)

Leave a comment