React useRef 완벽 가이드: DOM 접근부터 컴포넌트 통신까지
발행일: 2025년 12월 8일 오후 04:06
useRef의 핵심 사용법을 알아봅니다. DOM 직접 접근, 렌더링 없이 값 저장, 그리고 forwardRef와 useImperativeHandle을 활용한 부모-자식 컴포넌트 통신 패턴까지 실전 예제와 함께 설명합니다.
들어가며
React에서 useRef는 생각보다 자주 사용되지만, 정확히 언제 왜 써야 하는지 헷갈리는 Hook 중 하나입니다. 이번 글에서는 useRef의 핵심 사용법 3가지를 실전 예제와 함께 알아보겠습니다.
useRef란?
useRef는 렌더링에 영향을 주지 않는 값을 저장하는 React Hook입니다.
1const myRef = useRef(initialValue); 2 3// myRef.current → 저장된 값에 접근 4console.log(myRef.current);
핵심 특징: ref.current 값이 변경되어도 리렌더링이 발생하지 않습니다.
1. DOM 요소 직접 접근
가장 기본적인 사용법입니다. input에 자동 포커스, 스크롤 제어 등에 활용됩니다.
1function InputFocus() { 2 const inputRef = useRef<HTMLInputElement>(null); 3 4 const handleClick = () => { 5 // DOM 요소에 직접 접근 6 inputRef.current?.focus(); 7 }; 8 9 return ( 10 <> 11 <input ref={inputRef} placeholder="여기에 입력" /> 12 <button onClick={handleClick}>포커스 이동</button> 13 </> 14 ); 15}
실전 활용 예시
1function ScrollToTop() { 2 const topRef = useRef<HTMLDivElement>(null); 3 4 const scrollToTop = () => { 5 topRef.current?.scrollIntoView({ behavior: 'smooth' }); 6 }; 7 8 return ( 9 <div> 10 <div ref={topRef} /> 11 {/* ... 긴 컨텐츠 ... */} 12 <button onClick={scrollToTop}>맨 위로</button> 13 </div> 14 ); 15}
2. 값 저장 (렌더링 없이)
useState와 달리 값이 변경되어도 컴포넌트가 다시 렌더링되지 않습니다.
1function Timer() { 2 const [count, setCount] = useState(0); 3 const renderCount = useRef(0); 4 5 renderCount.current += 1; 6 7 return ( 8 <div> 9 <p>카운트: {count}</p> 10 <p>렌더링 횟수: {renderCount.current}</p> 11 <button onClick={() => setCount(c => c + 1)}>증가</button> 12 </div> 13 ); 14}
useState vs useRef 비교
| 특징 | useState | useRef |
|---|---|---|
| 값 변경시 | 리렌더링 O | 리렌더링 X |
| 용도 | UI에 표시되는 값 | 내부 로직용 값 |
| 접근 방법 | value | ref.current |
| 예시 | 입력값, 목록 | 타이머ID, DOM, 이전값 |
타이머 ID 저장 예시
1function StopWatch() { 2 const [time, setTime] = useState(0); 3 const timerRef = useRef<NodeJS.Timeout | null>(null); 4 5 const start = () => { 6 timerRef.current = setInterval(() => { 7 setTime(t => t + 1); 8 }, 1000); 9 }; 10 11 const stop = () => { 12 if (timerRef.current) { 13 clearInterval(timerRef.current); 14 } 15 }; 16 17 return ( 18 <div> 19 <p>{time}초</p> 20 <button onClick={start}>시작</button> 21 <button onClick={stop}>정지</button> 22 </div> 23 ); 24}
3. 부모 → 자식 데이터 가져오기
가장 강력한 패턴입니다. forwardRef와 useImperativeHandle을 조합하면 부모 컴포넌트에서 자식의 메서드를 직접 호출할 수 있습니다.
한마디로
ref = 자식한테 "야, 너 데이터 줘" 할 수 있는 통로
기본 구조
1// 자식이 노출할 메서드 타입 정의 2interface ChildRef { 3 getData: () => string[]; 4 reset: () => void; 5} 6 7// 자식 컴포넌트 8const ChildComponent = forwardRef<ChildRef, Props>((props, ref) => { 9 const [items, setItems] = useState<string[]>([]); 10 11 // 부모에게 노출할 메서드 정의 12 useImperativeHandle(ref, () => ({ 13 getData: () => items, 14 reset: () => setItems([]) 15 })); 16 17 return <div>{/* UI */}</div>; 18}); 19 20// 부모 컴포넌트 21function ParentComponent() { 22 const childRef = useRef<ChildRef>(null); 23 24 const handleSave = () => { 25 const data = childRef.current?.getData() || []; 26 console.log('자식 데이터:', data); 27 }; 28 29 return ( 30 <div> 31 <ChildComponent ref={childRef} /> 32 <button onClick={handleSave}>저장</button> 33 </div> 34 ); 35}
실전 예제: 폼 데이터 수집
1interface FormRef { 2 getFormData: () => { name: string; email: string }; 3 validate: () => boolean; 4 clear: () => void; 5} 6 7const InputForm = forwardRef<FormRef>((_, ref) => { 8 const [name, setName] = useState(''); 9 const [email, setEmail] = useState(''); 10 11 useImperativeHandle(ref, () => ({ 12 getFormData: () => ({ name, email }), 13 validate: () => name.length > 0 && email.includes('@'), 14 clear: () => { 15 setName(''); 16 setEmail(''); 17 } 18 })); 19 20 return ( 21 <div className="space-y-4"> 22 <input value={name} onChange={e => setName(e.target.value)} placeholder="이름" /> 23 <input value={email} onChange={e => setEmail(e.target.value)} placeholder="이메일" /> 24 </div> 25 ); 26}); 27 28function SubmitPage() { 29 const formRef = useRef<FormRef>(null); 30 31 const handleSubmit = async () => { 32 if (!formRef.current?.validate()) { 33 alert('입력값을 확인해주세요'); 34 return; 35 } 36 const data = formRef.current.getFormData(); 37 await submitData(data); 38 formRef.current.clear(); 39 }; 40 41 return ( 42 <div> 43 <h1>회원가입</h1> 44 <InputForm ref={formRef} /> 45 <button onClick={handleSubmit}>제출</button> 46 </div> 47 ); 48}
주의사항
1. 렌더링 중 ref 접근 금지
1// ❌ 잘못된 예 2function Bad() { 3 const ref = useRef<HTMLInputElement>(null); 4 const value = ref.current?.value; // 항상 null! 5 return <input ref={ref} />; 6} 7 8// ✅ 올바른 예 9function Good() { 10 const ref = useRef<HTMLInputElement>(null); 11 12 useEffect(() => { 13 console.log(ref.current?.value); // OK 14 }, []); 15 16 return <input ref={ref} />; 17}
2. Optional Chaining 필수
1// ❌ 에러 가능 2const data = myRef.current.getData(); 3 4// ✅ 안전한 접근 5const data = myRef.current?.getData() || [];
정리
| 용도 | 코드 |
|---|---|
| DOM 접근 | ref.current?.focus() |
| 값 저장 | ref.current = value |
| 자식 메서드 호출 | ref.current?.메서드() |
| 자식 메서드 노출 | forwardRef + useImperativeHandle |
핵심 기억하기:
useRef= 렌더링 없이 값을 기억하는 "상자"ref.current?.메서드()→ 자식이 노출한 메서드 호출forwardRef+useImperativeHandle→ 자식이 노출할 메서드 정의
이 글이 도움이 되셨다면 좋아요를 눌러주세요!
태그
#React#TypeScript#useRef#Hooks#forwardRef
0개의 댓글
24회 조회