메모이제이션은 동일한 입력에 대해 계산 결과나 참조를 재사용해 불필요한 렌더와 연산을 줄이는 방법입니다. React에서는 React.memo, useMemo, useCallback이 핵심 도구입니다. 다만 남용하면 복잡도와 메모리 비용이 증가하므로 측정과 목적에 맞게 적용하는 것이 중요합니다.
1. 언제 메모이제이션이 필요한가
다음 중 하나라도 해당되면 고려합니다: 연산 비용이 큰 계산이 매 렌더마다 반복되는 경우, 자식 컴포넌트가 동일한 데이터로 자주 렌더되는 경우, 리스트 항목 수가 많아 렌더 비용이 큰 경우, props로 객체/함수 참조가 자주 바뀌어 불필요한 재렌더를 유발하는 경우입니다.
2. React.memo로 컴포넌트 메모이제이션
React.memo는 props의 얕은 비교로 변경이 없으면 자식 렌더를 건너뜁니다. 함수나 객체 props는 참조가 바뀌면 동일해도 변경으로 간주되므로, useCallback/useMemo로 참조를 안정화합니다.
import React from 'react';
const TodoItem = React.memo(function TodoItem({ todo, onToggle, style }) {
console.log('render item', todo.id);
return (
<li style={style}>
<label>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.title}
</label>
</li>
);
});
export default TodoItem;커스텀 비교 함수는 정말 필요한 경우에만 사용합니다. 비교 비용이 렌더 비용보다 크면 역효과입니다.
3. useMemo로 값 메모이제이션 (파생 데이터/고비용 계산)
필터링, 정렬, 집계처럼 입력이 변하지 않으면 결과를 재사용합니다. 의존성 배열에 사용한 값만 정확히 넣습니다.
function useVisibleTodos(todos, query) {
const filtered = React.useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return todos;
return todos.filter(t => t.title.toLowerCase().includes(q));
}, [todos, query]);
const sorted = React.useMemo(() => {
return [...filtered].sort((a, b) => Number(a.done) - Number(b.done));
}, [filtered]);
return sorted;
}주의: useMemo는 캐시 힌트입니다. 메모이제이션 오버헤드가 계산 비용보다 크면 오히려 느려집니다.
4. useCallback으로 함수 참조 안정화
자식에 내려가는 이벤트 핸들러는 useCallback으로 참조를 고정합니다. 상태 업데이트는 함수형 업데이트를 사용해 의존성 배열을 최소화합니다.
function TodoList({ todos, setTodos }) {
const onToggle = React.useCallback((id) => {
setTodos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
}, [setTodos]);
const rowStyle = React.useMemo(() => ({ padding: 8 }), []); // 스타일 객체도 고정
return (
<ul>
{todos.map(t => (
<TodoItem key={t.id} todo={t} onToggle={onToggle} style={rowStyle} />
))}
</ul>
);
}5. 의존성 배열 정확히 관리 (stale closure 방지)
사용한 값은 반드시 의존성에 포함합니다. 의존성을 피하려고 []로 고정하면 오래된 값을 캡처할 수 있습니다. 상태 갱신은 함수형 업데이트로 해결합니다.
// 잘못된 예: count를 캡처해 항상 같은 값으로 증가
const [count, setCount] = React.useState(0);
const incWrong = React.useCallback(() => setCount(count + 1), []);
// 올바른 예: 최신 상태로 계산
const inc = React.useCallback(() => setCount(c => c + 1), []);6. 리스트 렌더링 최적화
항목 컴포넌트에 React.memo를 적용하고, key를 안정적인 고유 값으로 설정합니다. 긴 리스트는 react-window 같은 가상 스크롤을 함께 고려합니다. 항목에 내려가는 props 참조를 useMemo/useCallback으로 고정합니다.
7. Context와 메모이제이션
Context value가 새 객체면 모든 소비자가 재렌더링됩니다. value를 useMemo로 감싸고, 가능한 경우 컨텍스트를 쪼개거나 selector 기반 훅을 사용합니다.
const StoreContext = React.createContext(null);
function StoreProvider({ children }) {
const [state, dispatch] = React.useReducer(reducer, initial);
const value = React.useMemo(() => ({ state, dispatch }), [state]);
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}8. 측정과 검증 (도구 먼저)
React DevTools Profiler로 커밋 시간과 재렌더 원인을 확인합니다. 개발 중 why-did-you-render를 활용하면 불필요한 렌더를 빠르게 포착할 수 있습니다. 최적화 전후로 측정해 효과를 검증합니다.
// 개발 전용: why-did-you-render
import React from 'react';
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
const wdyr = require('@welldone-software/why-did-you-render');
wdyr(React, { trackAllPureComponents: true });
}9. 안티패턴 체크리스트
무조건 React.memo를 붙이지 않습니다. 아주 작은 컴포넌트나 값 변화가 빈번한 경우 오버헤드가 더 큽니다. useMemo로 API 응답을 캐싱하려 하지 말고 서버 상태 관리 라이브러리를 사용합니다. 커스텀 비교 함수로 깊은 비교를 매번 수행하는 것은 피합니다.
10. 실전 패턴 요약
자식 컴포넌트: React.memo를 기본으로 고려하고, props로 내려가는 객체/함수는 useMemo/useCallback으로 안정화합니다. 파생 데이터: useMemo로 계산 범위를 최소화하고, 계산 입력만 의존성에 포함합니다. 전역/Context: value를 useMemo로 고정하고, 필요 시 컨텍스트 분리 또는 selector를 도입합니다. 항상 Profiler로 확인합니다.
'React' 카테고리의 다른 글
| React와 Three.js를 이용한 3D 객체 렌더링 (1) | 2026.04.20 |
|---|---|
| React에서 Infinite Scroll 구현하기 (0) | 2026.04.20 |
| React에서 WebSocket 연결 구현하기 (0) | 2026.04.18 |
| React와 Redux Toolkit으로 상태 관리 구조화하기 (1) | 2026.04.17 |
| React Testing Library로 컴포넌트 단위 테스트 작성하기 (0) | 2026.04.17 |