[React] forwardRef 사용법, typescript 사용 시 발생하는 에러

현지렁이·2023년 3월 26일
1

React에서 ref prop은 HTML 엘리먼트에 직접 접근하기 위해서 사용된다. ref는 컴포넌트를 통해 자식 중 하나에 전달할 수 있다. 하지만 함수형 컴포넌트는 인스턴스가 없어서 ref 속성을 사용할수 없기 때문에, 커스텀한 컴포넌트로는 ref를 전달할 수 없다.

그래서 React Component에서 ref prop을 전달하기 위해서는 forwardRef()라는 함수를 사용해야 한다. React Component를 forwardRef() 함수로 감싸주면, 컴포넌트 함수는 2번째 매개변수를 갖게 되는데 이를 통해 ref prop을 넘길 수 있다.

forwardRef

커스텀 컴포넌트에서 ref를 통한 직접 제어가 필요할 때, React.forwardRef를 사용하면 부모 컴포넌트로부터 하위 컴포넌트로 ref를 전달할 수 있다. ref를 HTML 요소의 속성으로 넘겨줌으로써 함수 컴포넌트에서 ref를 통한 제어가 가능해진다.

// 커스텀한 입력 컴포넌트
<CustomInput
  type="text"
  ref={nameRef} // 함수 컴포넌트는 ref가 존재하지 않음!
></CustomInput>

커스텀 컴포넌트는 ref 속성이 존재하지 않아 위와 같이 ref를 전달받을 수 없다.

// forwardRef로 컴포넌트를 감싼 모습
const CustomInput = React.forwardRef(({ type, onKeyDown, placeholder }, ref) => {
  return (
    <input
      type={type}
      onKeyDown={onKeyDown}
      placeholder={placeholder}
      // 전달받은 ref는 HTML 속성으로 전달됩니다.
      ref={ref}
    ></input>
  );
});

export default CustomInput;

따라서 컴포넌트가 ref를 전달받기 위해서는 React.forwardRef 로 컴포넌트를 감싸준 후, props 뒤에 별도로 전달된 ref를 활용하면 된다.

display name

타입스크립트에서 forwardRef 사용 시 'Component definition is missing display name' 에러가 발생한다. 이는 forwardRef() 함수를 호출할 때 익명 함수를 넘기게 되면 브라우저에서 React 개발자 도구를 사용할 때 컴포넌트의 이름이 나오지 않아 발생한다. 이를 해결하기 위해서는 CustomInput.displayName = "CustomInput"; 와 같이 함수 호출의 결과를 displayName에 설정하거나, CustomInput = forwardRef(CustomInput); 와 같이 forwardRef() 함수의 호출 결과로 기존 컴포넌트를 대체하는 방법이 있다.

사용 예시

햄버거 메뉴 바깥 영역을 클릭하면 햄버거 메뉴가 닫히는 기능을 구현해야 했다.
하지만 햄버거 컴포넌트가 Nav바 내부에 위치하고 있기 때문에, 햄버거 메뉴 영역에 해당하는 ref를 Nav 컴포넌트에서 정의해준 후 Hamburger 컴포넌트에 props로 전달해서 제어해주어야 했다.
따라서 Hamburger 컴포넌트를 forwardRef로 감싸 전달받은 ref를 사용해주었다.

(-> 리팩토링 과정에서 '햄버거 메뉴 영역' ref를 Nav 컴포넌트 내에서만 사용하는 것으로 변경하여서, 현재는 forwardRef를 사용하지 않고 있다 ... )

// Nav.tsx
const Nav = () => {
  const [hamburger, setHamburger] = useState(false);
  // ✅ Ref 생성 (부모 컴포넌트)
  const sidebarRef = useRef<HTMLUListElement>(null); 
  
  const handleHamburger = () => {
    setHamburger((hamburger) => !hamburger);
  };
  
  const handleClickOutside = () => {
    setHamburger(false);
  };
  
  useOnClickOutside({ ref: sidebarRef, handler: handleClickOutside });
  
  return (
    <>
      <StHomeNav>
       ...
        <StHamburgerWrapper>
          ...
          <StHamburgerBtn type="button" onClick={handleHamburger}>
            {hamburger ? <IcClose /> : <IcHamburger />}
          </StHamburgerBtn>
        </StHamburgerWrapper>
        // ✅ Ref 전달
        <Hamburger isOpen={hamburger} ref={sidebarRef} />
      </StHomeNav>
    </>
  );
};

export default Nav;
// Hamburger.tsx
// ✅ forwardRef로 컴포넌트를 감싸줌
const Hamburger = React.forwardRef<HamburgerProps, HTMLUListElement>(({ isOpen }, ref) => {
  const navigate = useNavigate();

  return (
    <StOutsideHamburger isOpen={isOpen}>
      // ✅ 전달받은 ref (하위 컴포넌트)
      <StHamburgerWrapper isOpen={isOpen} ref={ref}>
        <StHamburgerMenu
          onClick={() => {
            navigate('/mypage');
          }}>
          회원 정보
        </StHamburgerMenu>
        <StHamburgerMenu
          onClick={() => {
            navigate('/library');
          }}>
          라이브러리
        </StHamburgerMenu>
      </StHamburgerWrapper>
    </StOutsideHamburger>
      );
});
// ✅ display name 설정
Hamburger.displayName = 'Hamburger';

export default Hamburger;

정리

커스텀 컴포넌트는 ref 속성이 존재하지 않아 ref를 전달받을 수 없다. 따라서 컴포넌트가 ref를 전달받기 위해서는 React.forwardRef 로 컴포넌트를 감싸준 후, props 뒤에 별도로 전달된 ref를 활용한다.

2개의 댓글

comment-user-thumbnail
2023년 4월 2일

ref 속성을 한 컴포넌트 내에서만 사용해보게 되어서, 커스텀 컴포넌트에 사용하는 방법은 몰랐는데 forwardRef 라는 속성으로 이렇게 전달하면 되는군요! 유익한 내용 잘 봤습니당 :)
혹시 타입스크립트에서 에러가 발생한다는 것은 자바스크립트에서는 displayName 설정을 하지 않아도 되는걸까요?

답글 달기
comment-user-thumbnail
2024년 4월 17일

forwardRef 제네릭은 Element가 먼저 오고 다음이 Props에요

답글 달기