forwardRef & useImperativeHandle

송윤서·2025년 3월 16일

Next.js

목록 보기
3/7
post-thumbnail

forwardRef

컴포넌트를 통해 자식 중 하나에 ref를 자동으로 전달하는 기법

사용해야하는 이유

  1. 함수형 컴포넌트는 인스턴트가 없기 때문에 ref 속성 사용 불가
    -> 함수형 컴포넌트를 forwardRef로 감싸주면 ref 사용 가능
  2. 부모 컴포넌트에서 자식 컴포넌트이 DOM element에 접근하고 싶을 때 사용

사용법

1. 부모 컴포넌트에서 useRef() 선언 -> 자식 컴포넌트에 보냄

import { useRef } from 'react'
import Child from './Child'

const Parent = () => {
  const compRef = useRef()
  
  return (
    <Child ref={compRef} />
  )
}

export default Parent;

2. 자식 컴포넌트를 forwardRef()로 감싸고 부모에서 사용할 함수를 useImperativeHandle()로 감싼다.

import { forwardRef, useImperativeHandle } from 'react'

const Child = forwardRef((props, ref) => {
  // 부모 컴포넌트에서 사용할 함수 설정
  useImperativeHandle(ref, () => ({
    req1,
    req2
  }))
  
  // 함수 1
  const req1 = () => { }
  // 함수 2
  const req2 = () => { }
}

export default Child

3. 부모 컴포넌트에서 current 프로퍼티를 통해 함수 사용

import { useRef } from 'react'
import Child from './Child'

const Parent = () => {
  const compRef = useRef()
  
  const fnReqBtn1 = e => {
    e.preventDefault();

    compRef.current.req1();
  }

  const fnReqBtn2 = e => {
    e.preventDefault();

    compRef.current.req2();
  }
  
  return (
    <>
      <Child ref={compRef} />
      <Link onClick={fnReqBtn1}>가입버튼1</Link>
      <Link onClick={fnReqBtn2}>가입버튼2</Link>
    </>
  )
}

export default Parent;

-> 자식 컴포넌트를 forwardRef로 감쌌을 때, 2번째 매개변수 ref는 부모 컴포넌트가 props로 넘긴 값 = compRef

사용예시


useImperativeHandle

React에서 부모 컴포넌트가 자식 컴포넌트의 내부 메서드나 상태를 직접 호출할 수 있도록 하는 Hook

  • 보통 forwardRef와 함께 사용

왜 사용하는 걸까?

  1. ref가 클래스 컴포넌트에 사용되는 것과 유사하게, 내부 메소드를 외부에 보낼 수 있습니다.

  2. 부모에게 꼭 자식의 실제 reference를 보내지 않고 우리가 원하는 일종의 proxy, reference를 보내는게 가능하므로 컴포넌트간 독립성을 보장할 수 있습니다.

사용법

1. 부모 컴포넌트에서 Ref 선언 -> 인스턴스 생성할 컴포넌트에 전달

export default function App() {
  const ref = useRef(null);

  console.log("parent");

  return (
    <div className="App">
      <p>Parent</p>

      <button>Click</button>
      <Input ref={ref} />
    </div>
  );
}

-> 이 때 자식 컴포넌트인 InputforwardRef를 통해 전달된 ref 획득

2. useImperativeHandle 훅을 사용해 인스턴스 생성

// Input Components
import { forwardRef, useImperativeHandle, useRef } from "react";

const Input = (props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(
    ref,
    () => ({
      myFocus: () => inputRef.current.focus(),
    }),
    [ref]
  );

  return <input placeholder="Child Input" {...props} ref={inputRef} />;
};

export default forwardRef(Input);

-> 부모 컴포넌트인 App에서 InputRef를 다룰 수 있는 메서드 생성

3. 부모 컴포넌트에서 메서드 접근

// App Components
export default function App() {
  const ref = useRef(null);

const handleClickFocus = () => {
    ref.current.myFocus();
  };

  return (
    <div className="App">
      <p>Parent</p>

      <button onClick={handleClickFocus}>Click</button>
      <Input ref={ref} />
    </div>
  );

-> 자식 컴포넌트의 DOM 노드 접근 가능

사용예시

// 모달을 컨트롤할 함수들을 정의
export type ModalRef<T> = {
  close: () => void;
  open: (data: T) => void;
};

// 모달 컴포넌트의 타입
type Props<T> = {
  children: (data: T) => React.ReactNode;
};

const Modal = forwardRef<ModalRef<any>, Props<any>>(({ children }, ref) => {
  const [isOpen, setIsOpen] = useState(false);
  const [data, setData] = useState<any>(null);

  // 모달 컨트롤 함수 정의
  useImperativeHandle(ref, () => ({
    close: () => {
      setIsOpen(false);
    },
    open: (data: any) => {
      // any 사용 또는 명시적인 제네릭 타입
      setIsOpen(true);
      setData(data);
    },
  }));

  if (!isOpen) return null;

  return createPortal(
    <div className="fixed inset-0 flex items-center justify-center z-50">
      <div className="absolute inset-0 bg-black opacity-40" />
      <div className="bg-white p-4 rounded-md relative z-10">
        {data && children(data)}
      </div>
    </div>,
    document.body
  );
});

Modal.displayName = "Modal";

export default Modal;

주의할 점

  1. 지나친 ref 사용은 지양할 것
    -> refprops가 수행할 수 없는 명령어를 처리하는 용도로만 사용
    Ex) 스크롤, 포커싱 등

createPortal

현재 컴포넌트를 다른 DOM으로 다른 위치에 렌더링할 수 있도록 해주는 기능

사용법

import { useEffect, useState } from "react";
import { createPortal } from "react-dom";

const Modal = ({ isOpen, onClose }) => {
  const [portalRoot, setPortalRoot] = useState(null);

  useEffect(() => {
    let root = document.getElementById("portal-root");
    if (!root) {
      root = document.createElement("div");
      root.id = "portal-root";
      document.body.appendChild(root);
    }
    setPortalRoot(root);
  }, []);

  if (!isOpen || !portalRoot) return null;

  return createPortal(
    <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
      <div className="bg-white p-6 rounded-lg">
        <h2>모달 창</h2>
        <button onClick={onClose}>닫기</button>
      </div>
    </div>,
    portalRoot
  );
};

export default Modal;

-> createPortal(children, container)
- children: 렌더링할 요소 (컴포넌트 내용)
- container: 해당 요소를 삽입할 DOM 노드
-> useEffect를 사용해 portal-root라는 <div>를 동적으로 생성하고 document.body에 추가
-> createPortal을 이용해, 모달을 portal-root에 렌더링
- 원래 부모 컴포넌트의 DOM 구조와 상관없이 body 아래에서 렌더링

profile
Front-end Developer

0개의 댓글