STUDY/[ React ]

forwardRef 사용법 - 장기 프로젝트

Lim임 2025. 11. 22. 22:11

React forwardRef 학습 노트

forwardRef란?

forwardRef는 React에서 부모 컴포넌트가 자식 컴포넌트의 DOM 요소에 직접 접근할 수 있도록 ref를 전달하는 고차 컴포넌트(Higher-Order Component) 함수입니다.


왜 필요한가?

일반 컴포넌트의 한계

React 컴포넌트는 기본적으로 props만 받을 수 있습니다. ref는 특별한 속성이므로 일반 props처럼 전달되지 않습니다.

// ❌ 일반 컴포넌트 - ref 전달 불가
const InputText = ({ value, onChange }: Props) => {
  return <input value={value} onChange={onChange} />;
};

// 부모 컴포넌트에서 사용
const Parent = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  return <InputText ref={inputRef} />; // ⚠️ 경고 발생, 작동 안 함
};

문제점:

  • ref는 props가 아니므로 컴포넌트 내부로 전달되지 않음
  • React DevTools에서 경고 메시지 출력
  • inputRef.current는 항상 null

forwardRef 사용법

기본 구조

const ComponentName = React.forwardRef<RefType, PropsType>(
  (props, ref) => {
    return <element ref={ref} {...props} />;
  }
);

실제 예제

import React from 'react';

type InputTextProps = {
  placeholder: string;
  value: string;
  onChange: (value: string) => void;
};

// ✅ forwardRef 사용 - ref 전달 가능
const InputText = React.forwardRef<HTMLInputElement, InputTextProps>(
  ({ placeholder, value, onChange }, ref) => {
    return (
      <input
        ref={ref}  // ← ref를 실제 DOM 요소에 전달
        type="text"
        placeholder={placeholder}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    );
  }
);

// displayName 설정 (디버깅용)
InputText.displayName = 'InputText';

export default InputText;

제네릭 타입 파라미터

React.forwardRef<T, P>

T (첫 번째 제네릭)

  • ref가 가리키는 DOM 요소의 타입
  • 예: HTMLInputElement, HTMLDivElement, HTMLButtonElement

P (두 번째 제네릭)

  • 컴포넌트가 받는 props의 타입
  • 예: InputTextProps, ButtonProps
// HTMLInputElement를 참조하는 InputText 컴포넌트
const InputText = React.forwardRef<HTMLInputElement, InputTextProps>(...);

// HTMLButtonElement를 참조하는 Button 컴포넌트
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(...);

// HTMLDivElement를 참조하는 Card 컴포넌트
const Card = React.forwardRef<HTMLDivElement, CardProps>(...);

부모 컴포넌트에서 사용하기

1. useRef로 ref 생성

import { useRef } from 'react';

const Parent = () => {
  // HTMLInputElement 타입의 ref 생성
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <InputText 
      ref={inputRef}  // ✅ forwardRef 덕분에 가능
      value={value}
      onChange={setValue}
    />
  );
};

2. ref를 통해 DOM 메서드 호출

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

  const focusInput = () => {
    inputRef.current?.focus();  // focus() 메서드 호출
  };

  const clearInput = () => {
    if (inputRef.current) {
      inputRef.current.value = '';
    }
  };

  return (
    <>
      <InputText ref={inputRef} />
      <button onClick={focusInput}>입력창 포커스</button>
      <button onClick={clearInput}>입력창 비우기</button>
    </>
  );
};

실전 예제: 엔터키 포커스 이동

시나리오

모달에서 첫 번째 입력창에서 Enter를 누르면 두 번째 입력창으로 포커스 이동

PlayerInputModal.tsx

import { useState, useRef } from 'react';
import InputText from './InputText';

const PlayerInputModal = () => {
  const [player1, setPlayer1] = useState('');
  const [player2, setPlayer2] = useState('');

  // 두 번째 input의 ref 생성
  const player2InputRef = useRef<HTMLInputElement>(null);

  const handleStartGame = () => {
    if (player1 && player2) {
      console.log('게임 시작!', { player1, player2 });
    }
  };

  return (
    <div>
      {/* 첫 번째 input: Enter 누르면 두 번째로 이동 */}
      <InputText
        placeholder="플레이어 1"
        value={player1}
        onChange={setPlayer1}
        onEnter={() => player2InputRef.current?.focus()}
      />

      {/* 두 번째 input: Enter 누르면 게임 시작 */}
      <InputText
        ref={player2InputRef}  // ✅ ref 연결
        placeholder="플레이어 2"
        value={player2}
        onChange={setPlayer2}
        onEnter={handleStartGame}
      />

      <button onClick={handleStartGame}>게임 시작</button>
    </div>
  );
};

InputText.tsx

import React from 'react';

type InputTextProps = {
  placeholder: string;
  value: string;
  onChange: (value: string) => void;
  onEnter?: () => void;
};

const InputText = React.forwardRef<HTMLInputElement, InputTextProps>(
  ({ placeholder, value, onChange, onEnter }, ref) => {
    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter' && onEnter) {
        onEnter();
      }
    };

    return (
      <input
        ref={ref}  // ✅ 부모로부터 받은 ref를 DOM에 전달
        type="text"
        placeholder={placeholder}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={handleKeyDown}
      />
    );
  }
);

InputText.displayName = 'InputText';

export default InputText;

displayName의 역할

InputText.displayName = 'InputText';

왜 필요한가?

forwardRef는 익명 함수를 반환하므로, React DevTools에서 컴포넌트 이름이 제대로 표시되지 않습니다.

// displayName 없이
<ForwardRef>
  <input />
</ForwardRef>

// displayName 설정 후
<InputText>
  <input />
</InputText>

디버깅 시 매우 유용합니다!


forwardRef의 주요 사용 사례

1. DOM 메서드 직접 호출

// focus(), blur(), scrollIntoView() 등
inputRef.current?.focus();
divRef.current?.scrollIntoView({ behavior: 'smooth' });

2. DOM 속성 읽기/쓰기

// value, scrollTop, offsetHeight 등
const height = divRef.current?.offsetHeight;
inputRef.current.value = 'new value';

3. 외부 라이브러리 연동

// 써드파티 라이브러리가 실제 DOM 필요할 때
useEffect(() => {
  if (canvasRef.current) {
    const chart = new Chart(canvasRef.current, config);
  }
}, []);

4. 애니메이션

// GSAP, Framer Motion 등과 함께 사용
useEffect(() => {
  gsap.to(boxRef.current, { x: 100, duration: 1 });
}, []);

TypeScript 타입 정의

ForwardedRef 타입

type ForwardedRef<T> = 
  | ((instance: T | null) => void)  // callback ref
  | RefObject<T | null>              // useRef로 만든 ref
  | null;

ForwardRefRenderFunction 타입

interface ForwardRefRenderFunction<T, P = {}> {
  (props: P, ref: ForwardedRef<T>): ReactNode;
  displayName?: string;
  propTypes?: any;
}

실제 사용 예

// 타입을 명시적으로 선언
const InputText: ForwardRefRenderFunction<
  HTMLInputElement, 
  InputTextProps
> = (props, ref) => {
  return <input ref={ref} {...props} />;
};

export default React.forwardRef(InputText);

주의사항

1. ref는 props가 아니다

// ❌ 이렇게 하면 안 됨
const InputText = ({ ref, ...props }: Props) => {
  return <input ref={ref} {...props} />;
};

// ✅ forwardRef 사용
const InputText = React.forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

2. null 체크 필수

// ❌ 위험: current가 null일 수 있음
inputRef.current.focus();

// ✅ 안전: optional chaining 사용
inputRef.current?.focus();

// ✅ 안전: 명시적 null 체크
if (inputRef.current) {
  inputRef.current.focus();
}

3. 함수형 컴포넌트에서만 사용 가능

forwardRef는 함수형 컴포넌트에서만 사용할 수 있습니다. 클래스 컴포넌트는 자동으로 ref를 지원합니다.


정리

항목 내용
목적 부모가 자식의 DOM에 접근하도록 ref 전달
문법 React.forwardRef<T, P>((props, ref) => {...})
제네릭 T ref가 가리키는 DOM 요소 타입
제네릭 P 컴포넌트 props 타입
displayName DevTools에서 컴포넌트 이름 표시
주요 용도 focus(), scrollIntoView() 등 DOM 메서드 호출
주의사항 null 체크 필수 (?. 사용)

참고 자료