React 최적화: useMemo와 useCallback, 언제 쓰고 언제 말아야 할까?

React로 애플리케이션을 개발하다 보면 "성능 최적화"라는 숙제에 직면하게 됩니다. 이때 가장 먼저 떠올리는 것이 useMemo와 useCallback이죠. 하지만 이 도구들은 무조건 많이 쓴다고 성능이 좋아지는 '마법의 지팡이'가 아닙니다. 오히려 잘못 사용하면 메모리 낭비와 코드 복잡성만 높일 수 있습니다.
오늘은 이 두 Hook의 정확한 사용 시점과 주의사항을 풍부한 예제를 통해 알아보겠습니다.
1. useMemo: 계산된 값의 재사용
useMemo는 특정 연산의 결과값을 메모리에 저장(메모이제이션)해 두었다가, 의존성 배열에 있는 값이 변경될 때만 다시 계산하는 Hook입니다.
✅ 언제 써야 할까?
A. 복잡하고 비용이 큰 계산이 포함될 때
데이터가 수만 개인 배열을 필터링하거나, 복잡한 수학적 계산을 렌더링마다 반복해야 한다면 useMemo가 효과적입니다.
import React, { useMemo } from 'react';
function ExpensiveComponent({ data, filterQuery }) {
// 데이터가 매우 클 경우, 렌더링마다 filter를 실행하는 것은 낭비입니다.
const filteredData = useMemo(() => {
console.log('복잡한 계산 수행 중...');
return data.filter(item => item.name.includes(filterQuery));
}, [data, filterQuery]); // data나 filterQuery가 바뀔 때만 다시 계산
return (
<ul>
{filteredData.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
B. 참조 동일성(Referential Integrity) 유지가 필요할 때
자식 컴포넌트가 React.memo로 감싸져 있고, 부모가 객체를 prop으로 전달할 때 중요합니다. JavaScript에서 객체는 내용이 같아도 참조 주소가 다르면 "다른 것"으로 인식되기 때문입니다.
❌ 언제 쓰지 말아야 할까?
- 단순한 연산: 변수 두 개를 더하거나, 간단한 문자열을 조합하는 정도는 useMemo를 사용하는 비용(메모리 할당 및 의존성 비교)이 계산 비용보다 더 클 수 있습니다.
- 의존성 배열이 너무 자주 바뀌는 경우: 매 렌더링마다 의존성이 바뀌어 어차피 매번 계산된다면 최적화의 의미가 없습니다.
2. useCallback: 함수의 재사용
useCallback은 값 대신 함수 자체를 메모이제이션합니다. 컴포넌트가 리렌더링될 때마다 함수가 새로 생성되는 것을 방지합니다.
✅ 언제 써야 할까?
A. React.memo와 함께 사용하는 자식 컴포넌트의 props로 함수를 전달할 때
이것이 useCallback의 가장 흔하고 중요한 용도입니다.
import React, { useState, useCallback } from 'react';
// 자식 컴포넌트: React.memo로 불필요한 리렌더링 방지
const SearchButton = React.memo(({ onClick }) => {
console.log('SearchButton 렌더링');
return <button onClick={onClick}>검색하기</button>;
});
function ParentComponent() {
const [text, setText] = useState('');
// useCallback이 없다면 부모가 렌더링될 때마다 handleSearch가 새로 생성됨
// 이로 인해 SearchButton도 (props가 바뀌었다고 판단하여) 리렌더링됨
const handleSearch = useCallback(() => {
console.log('검색 실행:', text);
}, [text]);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<SearchButton onClick={handleSearch} />
</div>
);
}
B. useEffect 내의 의존성으로 함수가 들어갈 때
함수 내부에서 상태값을 참조하고 그 함수를 useEffect에서 호출한다면, 함수의 참조값이 변할 때마다 효과가 실행되는 무한 루프를 방지하기 위해 사용합니다.
❌ 언제 쓰지 말아야 할까?
- 일반적인 이벤트 핸들러: React.memo를 쓰지 않는 일반 HTML 요소(button, input 등)에 전달하는 함수는 굳이 useCallback으로 감쌀 필요가 없습니다. 현대의 브라우저에서 함수 생성 자체는 매우 가벼운 작업입니다.
3. 최적화 도구 사용 전 체크리스트
최적화 코드를 작성하기 전에 스스로에게 다음 세 가지 질문을 던져보세요.
- "성능 저하가 실제로 체감되는가?"
- Chrome DevTools의 Profiler 탭을 사용하여 실제로 렌더링 병목이 발생하는지 확인하세요.
- "컴포넌트 구조를 먼저 개선할 수는 없는가?"
- 상태를 더 하위 컴포넌트로 내리거나(State Colocation), children prop을 활용해 컴포넌트 구성을 바꾸는 것만으로도 해결되는 경우가 많습니다.
- "메모이제이션 비용이 더 큰 것은 아닌가?"
- 모든 Hook 사용은 메모리 소비와 의존성 비교 로직이라는 비용을 수반합니다.
요약
도구목적주요 사용처
| useMemo | 값(Value)의 재사용 | 복잡한 데이터 처리, 객체의 참조 동일성 유지 |
| useCallback | 함수(Function)의 재사용 | React.memo 자식에게 전달하는 콜백 함수 |
| React.memo | 컴포넌트의 재사용 | Props가 변하지 않으면 리렌더링 건너뜀 |
성능 최적화는 '예방'보다 '진단'이 우선입니다. 코드의 가독성을 유지하면서 꼭 필요한 곳에 전략적으로 useMemo와 useCallback을 적용하는 것이 진정한 숙련자의 태도일 것입니다.