kimyenac
techreview
react
forwardRef 그리고 useImperativeHandle with React
2025-10-10

forwardRef 를 실무에서 제대로 써본 적이 없는 상황에서, PR 리뷰를 보며 궁금증이 생겨 공부한 내용을 정리합니다.
더불어 forwardRef 와 함께 많이 사용되는 useImperativeHandle hook 도 정리해보겠습니다.

참고로 forwardRef 는 react19 부터는 필요하지 않습니다! ref 를 props 로 전달할 수 있습니다.



forwardRef

forwardRef 는 React 에서 부모 컴포넌트가 하위 컴포넌트에게 자신의 ref 를 전달할 때 사용하는 기능입니다. 즉, 부모 컴포넌트가 자식 컴포넌트 내부의 DOM 요소나 특정 인스턴스에 직접 접근할 수 있게 해줍니다.



일반적으로 React 에서는 ref 를 DOM 요소에 직접 지정할 수 있지만, 함수형 컴포넌트에서는 ref 를 직접 지정할 수 없습니다. 이때 forwardRef 를 사용하면 부모의 ref 를 자식 내부의 실제 DOM 요소로 전달할 수 있습니다.

// forwardRef 를 사용한 ref 메커니즘
import type { ComponentProps } from 'react';
import { forwardRef, useRef } from 'react';

const Input = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
  (props, ref) => {
    return <input ref={ref} {...props} />;
  }
);

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const onFocus = () => inputRef.current?.focus();

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={onFocus}>Focus Input</button>
    </>
  );
}

하지만 위와 같이 forwardRef 를 쓰지 않아도 아래 예시 코드처럼 부모 컴포넌트가 하위 컴포넌트에 ref 를 전달할 수 있는데요.

// forwardRef 를 사용하지 않은 ref 메커니즘
import { Ref, useRef } from 'react';

const Input = ({ forwardedRef }: {forwardedRef: Ref<HTMLInputElement>}) => {
    return <input ref={forwardedRef} />
};

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const onFocus = () => inputRef.current?.focus();

  return (
    <>
      <Input forwardedRef={inputRef} />
      <button onClick={onFocus}>Focus Input</button>
    </>
  );
}

이렇게 작성해도 동작은 하지만, React 의 공식 ref 메커니즘이 아닙니다.
React 의 ref 시스템과 완전히 통합시키려면 React 의 특수 ref 전달 메커니즘인 forwardRef 를 사용하는 것이 좋겠습니다.






useImperativeHandle

useImperativeHandle은 forwardRef 랑 세트로 자주 쓰이는 React Hook 으로, 두 개를 같이 쓰면 부모가 자식의 내부 동작(메서드나 상태 제어)을 제한적으로 제어할 수 있도록 인터페이스를 노출할 수 있습니다.

useImperativeHandle(ref, createHandle, deps) 로 사용할 수 있습니다. (deps 는 배열로 옵셔널) 쉽게 말하면 자식 컴포넌트가 부모 컴포넌트에게 부모 컴포넌트가 쓸 수 있는 함수들을 설정할 때 사용하는 훅입니다.

import { forwardRef, useRef, useImperativeHandle, useState } from 'react';

const Modal = forwardRef((props, ref) => {
    const [isOpen, setIsOpen] = useState(false);

    // 2️⃣ 부모에게 노출할 메서드만 지정
    useImperativeHandle(ref, () => ({
        open: () => setIsOpen(true),
        close: () => setIsOpen(false),
    }));

    return isOpen ? <div>open</div> : <div>close</div>;
});

const App = () => {
  const modalRef = useRef<{ open: () => void; close: () => void }>(null);

  return (
    <>
      <button onClick={() => modalRef.current?.open()}>열기</button>
      <button onClick={() => modalRef.current?.close()}>닫기</button>
      <Modal ref={modalRef} />
    </>
  );
}

위와 같이 작성하면, 자식 컴포넌트의 내부 구현을 숨기고, 필요한 제어만 노출할 수 있고, DOM 직접 조작보다 컴포넌트 인터페이스 중심의 제어가 가능합니다.






언제 사용하면 좋을까? + 주의사항

단순 데이터 흐름으로 해결되는 경우, props 로 충분히 제어가 되는 경우엔 사용하지 말고, 부모가 자식의 DOM 이나 내부 메서드에 명령형으로 접근해야 할 경우, 함수형 컴포넌트에서도 ref 를 전달받아야 할 때 사용하면 됩니다.

적절한 상황에 forwardRef 와 useImperativeHandle 를 사용하여 캡슐화를 지키면서도 필요한 만큼만 안전하게 사용하면 되겠습니다.






Reference