241227 React forwardRef와 useImperativeHandle 완벽 가이드
27 Dec 2025
React forwardRef와 useImperativeHandle 완벽 가이드
React에서 부모 컴포넌트가 자식 컴포넌트의 내부 메서드에 직접 접근해야 하는 경우가 있습니다. 이때 forwardRef와 useImperativeHandle을 함께 사용하면 깔끔하고 타입 안전한 해결책을 제공할 수 있습니다.
기본 구현 예제
비디오 컴포넌트 예제
import { forwardRef, useRef, useImperativeHandle } from 'react';
// 타입 정의
interface MyVideo {
play: () => void;
pause: () => void;
}
// forwardRef 필수!
export const MyVideoComp = forwardRef<MyVideo>((props, ref) => {
const _video = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play() {
_video.current?.play();
},
pause() {
_video.current?.pause();
}
}));
return <video ref={_video} src="video.mp4" />;
});
// 사용
function MainPageComp() {
const _myVideo = useRef<MyVideo>(null);
const handlePlay = () => {
_myVideo.current?.play();
};
const handlePause = () => {
_myVideo.current?.pause();
};
return (
<div>
<MyVideoComp ref={_myVideo} />
<button onClick={handlePlay}>재생</button>
<button onClick={handlePause}>일시정지</button>
</div>
);
}
핵심 개념 설명
1. forwardRef
forwardRef는 부모 컴포넌트에서 전달받은 ref를 자식 컴포넌트 내부로 전달하는 HOC(Higher-Order Component)입니다.
// forwardRef 사용법
const MyComponent = forwardRef<RefType, PropsType>((props, ref) => {
// 컴포넌트 로직
return <div>...</div>;
});
2. useImperativeHandle
useImperativeHandle은 ref로 노출할 메서드들을 정의하는 훅입니다. 이를 통해 부모 컴포넌트가 자식 컴포넌트의 특정 기능에만 접근할 수 있도록 제한할 수 있습니다.
useImperativeHandle(ref, () => ({
// 노출할 메서드들
method1: () => { /* ... */ },
method2: () => { /* ... */ },
}));
실무 활용 사례
1. 모달 컴포넌트
interface ModalHandle {
open: () => void;
close: () => void;
toggle: () => void;
}
interface ModalProps {
children: React.ReactNode;
}
export const Modal = forwardRef<ModalHandle, ModalProps>(({ children }, ref) => {
const [isOpen, setIsOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(prev => !prev),
}));
if (!isOpen) return null;
return (
<div className="modal-backdrop">
<div className="modal-content">
{children}
<button onClick={() => setIsOpen(false)}>닫기</button>
</div>
</div>
);
});
// 사용
function App() {
const modalRef = useRef<ModalHandle>(null);
return (
<div>
<button onClick={() => modalRef.current?.open()}>
모달 열기
</button>
<Modal ref={modalRef}>
<h2>모달 내용</h2>
<p>여기에 내용을 넣으세요.</p>
</Modal>
</div>
);
}
2. 입력 필드 컴포넌트
interface InputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
setValue: (value: string) => void;
}
interface InputProps {
placeholder?: string;
defaultValue?: string;
}
export const CustomInput = forwardRef<InputHandle, InputProps>(
({ placeholder, defaultValue }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(defaultValue || '');
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
getValue: () => value,
setValue: (newValue: string) => setValue(newValue),
}));
return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
/>
);
}
);
// 사용
function Form() {
const nameInputRef = useRef<InputHandle>(null);
const emailInputRef = useRef<InputHandle>(null);
const handleSubmit = () => {
const name = nameInputRef.current?.getValue();
const email = emailInputRef.current?.getValue();
console.log('Name:', name);
console.log('Email:', email);
};
const handleClearAll = () => {
nameInputRef.current?.clear();
emailInputRef.current?.clear();
};
return (
<div>
<CustomInput ref={nameInputRef} placeholder="이름" />
<CustomInput ref={emailInputRef} placeholder="이메일" />
<button onClick={handleSubmit}>제출</button>
<button onClick={handleClearAll}>전체 지우기</button>
</div>
);
}
3. 애니메이션 컴포넌트
interface AnimationHandle {
play: () => void;
pause: () => void;
reset: () => void;
reverse: () => void;
}
interface AnimationProps {
duration?: number;
children: React.ReactNode;
}
export const AnimatedBox = forwardRef<AnimationHandle, AnimationProps>(
({ duration = 1000, children }, ref) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isReversed, setIsReversed] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
play: () => setIsPlaying(true),
pause: () => setIsPlaying(false),
reset: () => {
setIsPlaying(false);
setIsReversed(false);
},
reverse: () => {
setIsReversed(true);
setIsPlaying(true);
},
}));
return (
<div
ref={elementRef}
style={{
transform: isPlaying
? `translateX(${isReversed ? 0 : 100}px)`
: 'translateX(0)',
transition: `transform ${duration}ms ease-in-out`,
}}
>
{children}
</div>
);
}
);
// 사용
function AnimationDemo() {
const animationRef = useRef<AnimationHandle>(null);
return (
<div>
<AnimatedBox ref={animationRef}>
<div className="animated-content">애니메이션 박스</div>
</AnimatedBox>
<div className="controls">
<button onClick={() => animationRef.current?.play()}>재생</button>
<button onClick={() => animationRef.current?.pause()}>일시정지</button>
<button onClick={() => animationRef.current?.reset()}>리셋</button>
<button onClick={() => animationRef.current?.reverse()}>역재생</button>
</div>
</div>
);
}
베스트 프랙티스
1. 타입 안전성 확보
// 명확한 인터페이스 정의
interface ComponentHandle {
method1: () => void;
method2: (param: string) => string;
}
// forwardRef에 타입 명시
const Component = forwardRef<ComponentHandle, ComponentProps>(
(props, ref) => {
// 구현
}
);
2. 메서드 제한
DOM 요소의 모든 메서드를 노출하지 말고, 필요한 메서드만 선별적으로 노출하세요.
// ❌ 나쁜 예: 모든 DOM 메서드 노출
useImperativeHandle(ref, () => inputRef.current);
// ✅ 좋은 예: 필요한 메서드만 노출
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
}));
3. null 체크
ref를 사용할 때 항상 null 체크를 수행하세요.
const handleClick = () => {
// ref가 null일 수 있으므로 optional chaining 사용
myComponentRef.current?.method();
};
4. cleanup 함수 고려
복잡한 로직이 있는 경우 cleanup 함수를 제공하는 것을 고려하세요.
useImperativeHandle(ref, () => ({
start: () => { /* 시작 로직 */ },
stop: () => { /* 정지 로직 */ },
cleanup: () => { /* 정리 로직 */ },
}));
주의사항
-
남용 금지:
useImperativeHandle은 꼭 필요한 경우에만 사용하세요. 대부분의 경우 props와 콜백으로 충분합니다. -
선언적 패턴 우선: React의 선언적 패턴을 먼저 고려하고, 명령형 접근이 정말 필요한 경우에만 사용하세요.
-
컴포넌트 격리: ref를 통해 노출하는 메서드는 해당 컴포넌트의 책임 범위 내에서만 정의하세요.
언제 사용해야 할까?
- 포커스 관리 (input, modal 등)
- 애니메이션 제어
- 스크롤 위치 제어
- 미디어 재생 제어 (audio, video)
- 써드파티 라이브러리 래핑
forwardRef와 useImperativeHandle을 올바르게 사용하면 React의 선언적 특성을 해치지 않으면서도 필요한 명령형 API를 제공할 수 있습니다.