useMemo와 useCallback으로 불필요한 재계산·재생성을 방지하고, React.memo로 컴포넌트 리렌더링을 최적화하는 방법과 올바른 사용 기준을 배웁니다.
⚡ 성능 최적화는 꼭 필요할 때만
중요한 경고부터: useMemo, useCallback, React.memo는 남용하면 오히려 코드가 복잡해지고 성능이 나빠질 수 있습니다. 리액트는 기본적으로 충분히 빠릅니다. 이 훅들은 측정된 성능 문제가 있을 때만 적용하세요. 먼저 동작하게 만들고, 그다음 측정하고, 느리면 최적화합니다.
📦 useMemo — 비싼 계산 결과 캐싱
useMemo는 연산 결과를 메모이제이션(memoization)합니다. 의존성이 바뀌지 않으면 이전 계산 결과를 재사용합니다.
function ProductTable({ products, sortBy, filterText }: {
products: Product[]
sortBy: 'name' | 'price'
filterText: string
}) {
// filterText나 sortBy가 바뀔 때만 재계산
const processedProducts = useMemo(() => {
console.log('무거운 계산 실행됨') // 이 로그가 매 렌더마다 찍히면 문제
return products
.filter((p) => p.name.includes(filterText))
.sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
)
}, [products, sortBy, filterText])
return (
<table>
<tbody>
{processedProducts.map((p) => (
<tr key={p.id}>
<td>{p.name}</td>
<td>{p.price.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
)
}
products가 1만 개 이상이거나 계산이 복잡할 때 효과적입니다. 단순한 필터링/정렬은 useMemo 없어도 충분히 빠릅니다.
🔄 useCallback — 함수 재생성 방지
컴포넌트가 렌더링될 때마다 내부에서 선언한 함수는 새로 생성됩니다. useCallback은 함수 자체를 메모이제이션합니다:
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// ❌ 매 렌더마다 새 함수 생성 → ChildComponent가 불필요하게 리렌더
const handleSomething = () => console.log('처리')
// ✅ useCallback으로 함수 메모이제이션
const handleSomethingMemo = useCallback(() => {
console.log('처리')
}, []) // 의존성이 없으면 최초 한 번만 생성
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* ChildComponent는 handleSomethingMemo가 바뀔 때만 리렌더 */}
<ChildComponent onAction={handleSomethingMemo} />
</div>
)
}
useCallback이 효과를 내려면 받는 쪽 컴포넌트가 React.memo로 감싸져 있어야 합니다. 그렇지 않으면 useCallback은 의미가 없습니다.
🧱 React.memo — 컴포넌트 메모이제이션
React.memo로 감싼 컴포넌트는 Props가 바뀌지 않으면 리렌더링하지 않습니다:
// React.memo로 감싸기
const ExpensiveItem = React.memo(function ExpensiveItem({
name,
price,
onDelete,
}: {
name: string
price: number
onDelete: (name: string) => void
}) {
console.log(`${name} 렌더링`) // memo 없으면 부모 리렌더마다 출력됨
return (
<div className="flex justify-between p-3 border-b">
<span>{name} — {price.toLocaleString()}원</span>
<button onClick={() => onDelete(name)}>삭제</button>
</div>
)
})
🔬 최적화 결정 체크리스트
React.DevTools Profiler로 실제로 느린 컴포넌트를 확인했는가?- 계산이 진짜 무겁거나(수천 개 이상의 데이터 처리), 함수가 자주 재생성되어 자식 리렌더를 일으키는가?
- 최적화 후 실제로 빨라졌는가?
이 세 질문에 모두 "예"라면 적용합니다. 그렇지 않으면 코드를 단순하게 유지하세요.
✅ 정리
useMemo: 비싼 계산 결과를 캐싱. 의존성이 바뀔 때만 재계산.useCallback: 함수를 메모이제이션. React.memo와 함께 써야 효과적.React.memo: Props가 같으면 리렌더링 방지.- 측정 없는 최적화는 시기상조. 먼저 동작하게 만들고, 느리면 그때 최적화하세요.
관련 주제
- reducer 개념
- action dispatch
- useState 대비 장점
- 복잡한 상태 관리 시나리오
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
