ReactMedium#92
useCallback과 useMemo의 차이점과 각각의 적절한 사용 시나리오를 설명해주세요.
#React#useCallback#useMemo#최적화#메모이제이션
힌트
메모이제이션하는 대상(함수 vs 값)의 차이를 생각해보세요.
정답 및 해설
useCallback과 useMemo의 차이점과 각각의 적절한 사용 시나리오를 설명해주세요.
useCallback과 useMemo는 모두 **메모이제이션(memoization)**을 위한 React 훅으로, 불필요한 재계산이나 리렌더링을 방지합니다. useCallback은 함수 자체를 메모이제이션하고, useMemo는 함수의 실행 결과 값을 메모이제이션한다는 점이 핵심 차이입니다.
useCallback
기본 개념
// 기본 문법
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b] // 의존성 배열: a 또는 b가 변경될 때만 새 함수 생성
);
// useCallback(fn, deps) === useMemo(() => fn, deps)
// 사실상 useMemo의 특수한 형태
왜 필요한가?
// ❌ useCallback 없이 - 매 렌더링마다 새 함수 참조 생성
function ParentComponent() {
const [count, setCount] = useState(0);
// 렌더링마다 새로운 함수 객체가 생성됨
const handleClick = () => {
console.log('클릭!');
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
{/* count가 바뀔 때마다 ParentComponent 리렌더링 */}
{/* → handleClick이 새 참조로 변경 */}
{/* → ChildComponent도 리렌더링 (불필요) */}
<ChildComponent onClick={handleClick} />
</div>
);
}
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent 렌더링');
return <button onClick={onClick}>자식 버튼</button>;
});
// ✅ useCallback 사용 - 함수 참조 유지
function ParentComponent() {
const [count, setCount] = useState(0);
// count가 변경되어도 handleClick의 참조는 유지됨
const handleClick = useCallback(() => {
console.log('클릭!');
}, []); // 의존성 없음 - 컴포넌트 생애주기 동안 동일한 참조
return (
<div>
<button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
{/* count 변경 → ParentComponent 리렌더링 */}
{/* → handleClick 참조 동일 */}
{/* → ChildComponent 리렌더링 스킵! */}
<ChildComponent onClick={handleClick} />
</div>
);
}
의존성 배열 관리
function SearchComponent({ userId }) {
const [query, setQuery] = useState('');
// userId나 query가 변경될 때만 새 함수 생성
const handleSearch = useCallback(async () => {
const results = await searchAPI(userId, query);
setResults(results);
}, [userId, query]); // 사용된 외부 값은 모두 의존성에 추가
// ❌ 잘못된 예: 의존성 누락
const handleSearch = useCallback(async () => {
const results = await searchAPI(userId, query); // query 사용
setResults(results);
}, [userId]); // query 누락 → stale closure 버그!
return (
<SearchInput onSearch={handleSearch} />
);
}
useMemo
기본 개념
// 기본 문법
const memoizedValue = useMemo(
() => expensiveCalculation(a, b), // 계산 함수
[a, b] // 의존성 배열: a 또는 b가 변경될 때만 재계산
);
비용이 큰 연산에 사용
function ProductList({ products, filter, sortOrder }) {
// ❌ useMemo 없이 - 렌더링마다 필터링/정렬 재실행
const processedProducts = products
.filter(p => p.category === filter)
.sort((a, b) => sortOrder === 'asc' ? a.price - b.price : b.price - a.price);
// ✅ useMemo 사용 - products, filter, sortOrder 중 하나가 변경될 때만 재계산
const processedProducts = useMemo(() => {
console.log('필터링/정렬 실행'); // 실제로 필요할 때만 실행
return products
.filter(p => p.category === filter)
.sort((a, b) => sortOrder === 'asc' ? a.price - b.price : b.price - a.price);
}, [products, filter, sortOrder]);
return (
<ul>
{processedProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
객체/배열 참조 안정화
// useMemo로 객체 참조 안정화
function UserDashboard({ userId, theme }) {
// ❌ 매 렌더링마다 새로운 객체 생성 → 하위 컴포넌트가 불필요하게 리렌더링
const config = {
userId,
theme,
timestamp: Date.now()
};
// ✅ theme나 userId가 변경될 때만 새 객체 생성
const config = useMemo(() => ({
userId,
theme,
timestamp: Date.now()
}), [userId, theme]);
return <Dashboard config={config} />;
}
useCallback vs useMemo 비교
// 둘은 사실 같은 메커니즘
const memoizedFn = useCallback(() => doSomething(a), [a]);
// 위와 동일한 동작
const memoizedFn = useMemo(() => () => doSomething(a), [a]);
// useCallback: 함수 자체를 반환
// useMemo: 함수 실행 결과를 반환
| 항목 | useCallback | useMemo |
|---|---|---|
| 메모이제이션 대상 | 함수 자체 | 함수 실행 결과 값 |
| 반환값 | 메모이제이션된 함수 | 계산된 값 |
| 주 사용 목적 | 자식 컴포넌트에 안정적인 함수 참조 전달 | 비용이 큰 연산 결과 캐싱 |
| 의존성 변경 시 | 새 함수 생성 | 값 재계산 |
실제 사용 시나리오
시나리오 1: 커스텀 훅에서 함수 안정화
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// 매 렌더링마다 새 함수가 생성되지 않도록 메모이제이션
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
// 이 훅의 사용자는 increment 참조가 안정적임을 보장받음
function MyComponent() {
const { count, increment } = useCounter(0);
// increment를 useEffect 의존성에 넣어도 무한루프 없음
useEffect(() => {
const timer = setInterval(increment, 1000);
return () => clearInterval(timer);
}, [increment]); // increment 참조가 안정적이므로 안전
}
시나리오 2: 데이터 변환 메모이제이션
function AnalyticsDashboard({ rawData, dateRange }) {
// 비용이 큰 데이터 처리 (집계, 통계 계산)
const chartData = useMemo(() => {
return rawData
.filter(d => isInRange(d.date, dateRange))
.reduce((acc, item) => {
const date = formatDate(item.date);
acc[date] = (acc[date] || 0) + item.value;
return acc;
}, {});
}, [rawData, dateRange]);
// 파생 통계 계산
const statistics = useMemo(() => {
const values = Object.values(chartData);
return {
total: values.reduce((a, b) => a + b, 0),
average: values.reduce((a, b) => a + b, 0) / values.length,
max: Math.max(...values),
min: Math.min(...values),
};
}, [chartData]); // chartData가 변경될 때만 재계산
return (
<div>
<Chart data={chartData} />
<StatsSummary stats={statistics} />
</div>
);
}
시나리오 3: Context와 함께 사용
// Context value 안정화
const ThemeContext = React.createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ❌ 매 렌더링마다 새 객체 생성 → 모든 Context 소비자 리렌더링
const value = { theme, setTheme };
// ✅ theme가 변경될 때만 새 객체 생성
const value = useMemo(
() => ({ theme, setTheme }),
[theme] // setTheme은 useState의 setter라 항상 동일한 참조
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
과도한 사용의 위험성
// ❌ 불필요한 useMemo - 단순 값에는 오버헤드만 증가
const sum = useMemo(() => a + b, [a, b]);
// → 그냥 const sum = a + b; 가 더 빠름
// ❌ 불필요한 useCallback - 자식이 memo가 아니면 의미 없음
function Parent() {
const handleClick = useCallback(() => {}, []);
// ChildComponent가 React.memo로 감싸져 있지 않으면 무의미
return <ChildComponent onClick={handleClick} />;
}
// 메모이제이션 자체도 비용이 있음:
// - 이전 값 저장을 위한 메모리 사용
// - 의존성 비교 연산 수행
// - 코드 복잡도 증가
언제 사용해야 할까?
useCallback을 써야 할 때:
✅ React.memo로 감싼 자식 컴포넌트에 함수를 prop으로 전달할 때
✅ useEffect, useMemo 등의 의존성 배열에 함수를 포함해야 할 때
✅ 비용이 큰 외부 API 호출을 트리거하는 함수
✅ 커스텀 훅에서 안정적인 API 제공 시
useMemo를 써야 할 때:
✅ 대용량 데이터 필터링/정렬/변환 시
✅ 복잡한 수학적 계산 시
✅ React.memo 자식에 전달할 객체/배열의 참조 안정화
✅ Context value 안정화
사용하지 않아도 될 때:
❌ 단순한 계산 (덧셈, 문자열 연결 등)
❌ React.memo로 감싸지 않은 자식에 전달하는 함수
❌ 렌더링 성능 문제가 실제로 발생하지 않은 경우
React DevTools Profiler로 확인
// 성능 문제가 의심될 때 먼저 프로파일링 후 최적화
// React DevTools → Profiler 탭 → Record
// 컴포넌트가 리렌더링된 이유 확인
// "Why did this render?" 기능 활용
정리
| 항목 | useCallback | useMemo |
|---|---|---|
| 목적 | 함수 참조 안정화 | 계산 결과 캐싱 |
| 반환 | 메모이제이션된 함수 | 계산된 값 (any type) |
| 주 사용처 | memo 자식에 콜백 전달 | 비용이 큰 연산 |
| 함수 실행 여부 | 직접 실행 안 함 (참조만) | 즉시 실행하여 결과 저장 |
| 동등 표현 | useCallback(fn, deps) | useMemo(() => fn, deps) |
| 남용 시 영향 | 메모리 사용 증가 | 메모리 사용 증가 |
핵심: 두 훅 모두 "먼저 문제가 있는지 확인하고, 있을 때만 최적화"하는 원칙을 지켜야 합니다. React DevTools Profiler로 실제 렌더링 성능 문제를 확인한 후 적용하세요.