C
React/고급/Lesson 21

forwardRef와 useImperativeHandle

25분·theory
이 챕터
2/2
TypeScript

forwardRef와 useImperativeHandle

💡 왜 배워야 할까요?

🎯 라이브러리 개발자가 반드시 알아야 할 고급 기술입니다.
💼 비제어 컴포넌트를 만들 때 필수입니다.
대기업 기술 면접에서 자주 출제되는 개념입니다.
🏢 실무에서는
UI 라이브러리를 만들 때, 부모 컴포넌트가 자식의 input을 직접 제어해야 할 수 있습니다. 예를 들어 '포커스 맞추기', '값 초기화하기' 같은 명령형 작업이 필요할 때 forwardRef와 useImperativeHandle을 사용합니다. 이는 라이브러리 개발자의 필수 스킬입니다.

forwardRef란?

forwardRef와 useImperativeHandle

문제: ref는 props로 전달 불가

기본적으로 ref는 특수 prop이라 자식 컴포넌트로 전달되지 않습니다.

forwardRef

부모가 전달한 ref를 자식 컴포넌트 내부의 DOM 또는 인스턴스에 연결합니다.

useImperativeHandle

forwardRef와 함께 사용해 부모에게 노출할 인터페이스를 명시적으로 정의합니다.

사용 사례

  • 커스텀 Input 컴포넌트의 focus() 제어
  • 스크롤 위치 제어
  • 폼 라이브러리 통합 (React Hook Form의 Controller)
  • 재사용 컴포넌트 라이브러리 개발

React 19 변경

React 19부터 forwardRef 없이 ref를 props로 받을 수 있습니다.

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

// 1. 기본 forwardRef: DOM ref 전달
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

// forwardRef<ref 타입, props 타입>
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div className="flex flex-col gap-1">
        {label && <label className="text-sm font-medium">{label}</label>}
        {/* ref를 실제 input DOM에 연결 */}
        <input
          ref={ref}
          className={`border rounded px-3 py-2 ${
            error ? 'border-red-500' : 'border-gray-300'
          }`}
          {...props}
        />
        {error && <p className="text-red-500 text-xs">{error}</p>}
      </div>
    );
  }
);

CustomInput.displayName = 'CustomInput'; // DevTools 표시명

// 부모 컴포넌트에서 ref로 input DOM 직접 접근
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const focusEmail = () => emailRef.current?.focus(); // 외부에서 포커스 제어

  return (
    <form>
      <CustomInput ref={emailRef} label="이메일" type="email" />
      <CustomInput ref={passwordRef} label="비밀번호" type="password" />
      <button type="button" onClick={focusEmail}>이메일로 포커스</button>
    </form>
  );
}

// 2. useImperativeHandle: 노출 API 커스터마이징
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (seconds: number) => void;
  getCurrentTime: () => number;
}

interface VideoPlayerProps {
  src: string;
  autoPlay?: boolean;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  ({ src, autoPlay = false }, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);

    // 부모에게 노출할 메서드 정의 (DOM 전체가 아닌 선택된 API만 노출)
    useImperativeHandle(ref, () => ({
      play: () => videoRef.current?.play(),
      pause: () => videoRef.current?.pause(),
      seek: (seconds) => {
        if (videoRef.current) videoRef.current.currentTime = seconds;
      },
      getCurrentTime: () => videoRef.current?.currentTime ?? 0,
    }), []); // 의존성 배열

    return (
      <video
        ref={videoRef}
        src={src}
        autoPlay={autoPlay}
        controls
        className="w-full rounded"
      />
    );
  }
);

// 부모: 노출된 API만 사용 가능 (DOM 전체 접근 불가)
function VideoPage() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <div className="flex gap-2 mt-2">
        <button onClick={() => playerRef.current?.play()}>재생</button>
        <button onClick={() => playerRef.current?.pause()}>정지</button>
        <button onClick={() => playerRef.current?.seek(30)}>30초로 이동</button>
      </div>
    </div>
  );
}

// 3. React 19 방식 (forwardRef 불필요)
// function NewInput({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
//   return <input ref={ref} {...props} />;
// }

💡 실전 팁

  • displayName: forwardRef 컴포넌트에 displayName 설정 → React DevTools에서 이름 표시
  • useImperativeHandle 권장: DOM 전체 노출 대신 필요한 메서드만 노출해 캡슐화
  • React 19: ref를 일반 prop으로 전달 가능, forwardRef 불필요 (하위 호환 유지)
  • React Hook Form: Controller 컴포넌트 내부에서 forwardRef 활용
  • TypeScript: forwardRef<RefType, PropsType> 제네릭으로 타입 안전하게 사용

⚛️ React 패턴 — forwardRef와 useImperativeHandle

forwardRef와 useImperativeHandle을 React에서 어떻게 쓰는지 코드와 함께 단계별로 익혀보세요.
1 🧩 1. forwardRef와 useImperativeHandle 사용 시나리오
이 기능이 필요한 상황.
2 💻 2. 코드 작성
forwardRef와 useImperativeHandle의 기본 사용법.
3 🎨 3. 렌더링 결과
사용자가 보는 화면.
4 💡 4. 실무 팁
흔한 함정과 베스트 프랙티스.

🎮 forwardRef와 useImperativeHandle — 단계별 이해

각 단계를 클릭해 내용을 읽고 ✓ 이해했어요 버튼으로 진행률을 확인하세요.
🖥️ 실행 결과 — 렌더링된 React 컴포넌트
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — 결과 먼저 확인 후 코드 편집기에서 직접 수정해볼 수 있습니다.

확인 퀴즈

forwardRef가 필요한 상황은?
💡 함수형 컴포넌트는 기본적으로 ref를 받을 수 없어요. forwardRef로 감싸면 부모의 ref를 자식 컴포넌트가 받아 내부 DOM 요소에 연결할 수 있어요. UI 라이브러리 컴포넌트에서 자주 사용해요.
먼저 읽으면 좋은 개념: Error Boundary + Suspense
다음 추천: TypeScript + React
forwardRef + useImperativeHandle - React