useRef로 DOM 요소에 직접 접근하고, 렌더링 없이 값을 유지하는 방법, forwardRef 패턴까지 useRef의 두 가지 역할을 완전히 이해합니다.
🔍 useRef — 두 가지 역할
useRef는 용도가 두 가지입니다. 혼동하기 쉬우니 처음부터 명확히 구분합니다:
- DOM 요소에 직접 접근: 특정 DOM 요소를 직접 제어할 때 (포커스, 스크롤, 크기 측정 등)
- 렌더링 없이 값 유지: 리렌더링을 일으키지 않고 값을 기억해야 할 때 (이전 값 저장, 타이머 ID 등)
📍 역할 1 — DOM 요소에 접근하기
입력창 자동 포커스, 스크롤 위치 제어 같은 작업은 DOM에 직접 접근해야 합니다. useRef로 요소에 연결합니다:
import { useRef, useEffect } from 'react'
function SearchModal({ isOpen }: { isOpen: boolean }) {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// 모달이 열릴 때 자동으로 입력창에 포커스
if (isOpen && inputRef.current) {
inputRef.current.focus()
}
}, [isOpen])
return (
<div>
{/* ref 속성으로 DOM 요소와 연결 */}
<input
ref={inputRef}
type="search"
placeholder="검색..."
className="border rounded px-4 py-2 w-full"
/>
</div>
)
}
inputRef.current가 실제 DOM 요소입니다. useEffect 없이 렌더링 중에 inputRef.current를 읽으면 null입니다 — DOM은 렌더링 후에 연결됩니다.
💾 역할 2 — 렌더링 없이 값 유지
useState는 값이 바뀌면 리렌더링을 일으킵니다. 그런데 때로는 값은 기억하되, 리렌더링은 일으키고 싶지 않을 때가 있습니다. 타이머 ID가 대표적입니다:
function Stopwatch() {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
function start() {
if (isRunning) return
setIsRunning(true)
intervalRef.current = setInterval(() => {
setTime((prev) => prev + 1)
}, 1000)
}
function stop() {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setIsRunning(false)
}
function reset() {
stop()
setTime(0)
}
return (
<div className="text-center space-y-4">
<p className="text-4xl font-mono font-bold">{time}초</p>
<div className="flex gap-2 justify-center">
{!isRunning ? (
<button onClick={start} className="px-4 py-2 bg-green-500 text-white rounded">시작</button>
) : (
<button onClick={stop} className="px-4 py-2 bg-red-500 text-white rounded">정지</button>
)}
<button onClick={reset} className="px-4 py-2 bg-gray-200 rounded">초기화</button>
</div>
</div>
)
}
intervalRef를 useState로 관리하면 타이머 ID가 바뀔 때마다 불필요한 리렌더링이 발생합니다. useRef로 관리하면 값은 유지되지만 리렌더링 없이 조용히 저장됩니다.
⏪ 이전 값 기억하기
useRef로 이전 렌더의 값을 기억하는 유용한 패턴입니다:
function PriceDisplay({ price }: { price: number }) {
const prevPriceRef = useRef<number>(price)
useEffect(() => {
prevPriceRef.current = price // 렌더 후 이전 값 업데이트
})
const prevPrice = prevPriceRef.current
const change = price - prevPrice
return (
<div>
<span className="text-2xl font-bold">{price.toLocaleString()}원</span>
{change !== 0 && (
<span className={change > 0 ? 'text-red-500' : 'text-blue-500'}>
{change > 0 ? '▲' : '▼'} {Math.abs(change).toLocaleString()}
</span>
)}
</div>
)
}
🔗 forwardRef — ref를 자식 컴포넌트로 전달
직접 만든 컴포넌트에는 ref를 기본으로 전달할 수 없습니다. forwardRef로 감싸야 합니다:
import { forwardRef } from 'react'
interface InputProps {
label: string
placeholder?: string
}
// forwardRef로 ref를 받아서 실제 input 요소로 전달
const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ label, placeholder }, ref) => {
return (
<label className="block">
<span className="text-sm font-medium text-gray-700">{label}</span>
<input
ref={ref} // 전달받은 ref를 DOM에 연결
placeholder={placeholder}
className="mt-1 block w-full border rounded px-3 py-2"
/>
</label>
)
})
CustomInput.displayName = 'CustomInput' // DevTools 표시명
// 사용
function ParentForm() {
const nameInputRef = useRef<HTMLInputElement>(null)
function focusName() {
nameInputRef.current?.focus()
}
return (
<div>
<CustomInput ref={nameInputRef} label="이름" />
<button onClick={focusName}>이름 입력창 포커스</button>
</div>
)
}
✅ 정리
useRef는 ① DOM 요소 접근, ② 렌더링 없이 값 유지 두 가지 역할입니다.ref.current로 값에 접근합니다. DOM ref는 렌더링 후에야 연결됩니다.- 타이머 ID, 이전 값 등 리렌더링 없이 유지할 값에 사용합니다.
- 직접 만든 컴포넌트에 ref를 전달하려면
forwardRef로 감쌉니다.
관련 주제
- composition 패턴
- children 슬롯
- 재사용 가능한 컴포넌트 설계
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
