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 체크 필수 (?. 사용) |