useRef: DOM 참조
import { useRef, useEffect } from "react";
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 마운트 후 입력창에 포커스
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="자동 포커스" />;
}
useRef: 렌더링 없이 값 저장
렌더링을 유발하지 않고 값을 유지합니다.
function StopWatch() {
const [time, setTime] = useState(0);
const [running, setRunning] = useState(false);
const intervalRef = useRef<number | null>(null);
function start() {
if (running) return;
setRunning(true);
intervalRef.current = window.setInterval(() => {
setTime(t => t + 1);
}, 1000);
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current);
setRunning(false);
}
// intervalRef.current 변경은 리렌더링을 유발하지 않음
return (
<div>
<p>{time}초</p>
<button onClick={start}>시작</button>
<button onClick={stop}>정지</button>
</div>
);
}
useMemo: 비싼 계산 캐싱
import { useMemo, useState } from "react";
function FilteredProducts({ products }: { products: Product[] }) {
const [query, setQuery] = useState("");
const [minPrice, setMinPrice] = useState(0);
// query나 minPrice, products가 바뀔 때만 재계산
const filtered = useMemo(() => {
console.log("필터링 실행"); // 렌더링마다 실행되지 않음
return products
.filter(p => p.name.includes(query))
.filter(p => p.price >= minPrice);
}, [products, query, minPrice]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<input type="number" value={minPrice} onChange={e => setMinPrice(Number(e.target.value))} />
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
</div>
);
}
useCallback: 함수 참조 안정화
import { useCallback, memo } from "react";
// memo: props가 같으면 리렌더링 건너뜀
const ExpensiveChild = memo(({ onAction }: { onAction: () => void }) => {
console.log("자식 렌더링");
return <button onClick={onAction}>액션</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// useCallback 없이: 매 렌더링마다 새 함수 → 자식도 리렌더링
// useCallback 사용: 함수 참조 유지 → 자식 리렌더링 없음
const handleAction = useCallback(() => {
console.log("액션 실행");
}, []); // 의존성 없으면 최초 1회만 생성
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>{count}</p>
<ExpensiveChild onAction={handleAction} />
</div>
);
}
언제 최적화가 필요한가?
flowchart TB
Q{"성능 문제가\n실제로 있는가?"}
Q -->|"아니오"| SKIP["최적화 건너뜀\n코드 복잡도 증가"]
Q -->|"예"| PROFILE["React DevTools로\n프로파일링"]
PROFILE --> SLOW["느린 컴포넌트 발견"]
SLOW --> MEMO["memo, useMemo,\nuseCallback 적용"]
성능 최적화 원칙:
- 먼저 동작하게 만들고
- 측정 후 필요할 때만 최적화
useId: 고유 ID 생성
import { useId } from "react";
function FormField({ label }: { label: string }) {
const id = useId(); // 서버-클라이언트 일관된 고유 ID
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
정리
| Hook | 용도 |
|---|---|
useRef | DOM 참조, 렌더링 없는 값 저장 |
useMemo | 비싼 계산 결과 캐싱 |
useCallback | 함수 참조 안정화 |
memo() | props 변화 없으면 리렌더링 건너뜀 |
useId | 고유 ID 생성 |
다음 편에서는 Context API — prop drilling 없이 전역 상태를 공유하는 방법을 배웁니다.