useCallback이 함수를 위한 메모이제이션 도구였다면, useMemo는 값(value)을 위한 도구였다.
댄은 이 새로운 훅의 필요성을 설명하기 위해, 또 다른 시나리오를 제시했다.
수천 개의 항목을 가진 거대한 리스트를 화면에 보여주는 컴포넌트였다. 이 컴포넌트는 사용자가 입력한 검색어에 따라 리스트를 필터링하고, 정렬 기준에 따라 순서를 바꾸는 복잡한 기능을 가지고 있었다.
function ProductList({ products, searchTerm, sortBy }) {
// 1. 검색어로 제품을 필터링한다.
const filteredProducts = products.filter((p) => p.name.includes(searchTerm));
// 2. 정렬 기준에 따라 필터링된 제품을 정렬한다.
const sortedProducts = filteredProducts.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
// 3. 화면에 보여준다.
return (
<ul>
{sortedProducts.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
이 코드의 문제는, 필터링(filter)과 정렬(sort)이라는 ‘비싼’ 계산이, 컴포넌트가 렌더링될 때마다 매번 실행된다는 점이었다.
수천 개의 제품을 필터링하고 정렬하는 것은 상당한 연산 자원을 소모했다. 만약 이 컴포넌트에 리스트와는 아무 관련 없는 다른 상태(예: 다크 모드 토글)가 있고, 그 상태가 바뀌어 리렌더링이 일어난다면?
products, searchTerm, sortBy 값이 전혀 바뀌지 않았음에도 불구하고, 이 비싼 계산은 불필요하게 다시 실행될 터였다. 애플리케이션은 사용자의 간단한 클릭 한 번에 잠시 멈칫하는 듯한, 미세한 버벅임을 보일 수 있었다.
“이럴 때 필요한 것이 useMemo입니다.”
댄이 코드를 수정하며 말했다.
function ProductList({ products, searchTerm, sortBy }) {
// 비싼 계산 로직을 useMemo로 감싼다.
const sortedProducts = useMemo(() => {
console.log("Calculating products..."); // 계산이 실행될 때만 로그 출력
const filteredProducts = products.filter(p => p.name.includes(searchTerm));
const sortedProducts = filteredProducts.sort((a, b) => {
// ... 정렬 로직 ...
});
return sortedProducts;
}, [products, searchTerm, sortBy]); // 계산에 사용된 모든 값을 의존성 배열에 넣는다.
return (
// ...
);
}
useMemo의 구조는 useCallback과 거의 동일했다.
첫 번째 인자로는 값을 생성하는 ‘팩토리 함수(factory function)’를, 두 번째 인자로는 의존성 배열을 받았다.
useMemo는 리액트에게 이렇게 말했다.
“내가 전달한 이 팩토리 함수를 실행해서 그 결과 값을 기억해둬. 그리고 다음 렌더링 때, 이 의존성 배열의 값이 바뀌지 않는 한, 함수를 다시 실행하지 말고 이전에 기억해뒀던 바로 그 값을 그대로 돌려줘.”
이제, ProductList 컴포넌트가 다른 이유로 리렌더링되어도, products, searchTerm, sortBy 중 어느 하나도 바뀌지 않았다면 useMemo는 내부의 비싼 계산을 건너뛰었다. 콘솔에는 Calculating products... 로그가 더 이상 찍히지 않았다.
useMemo는 불필요한 연산을 막아주는 강력한 성능 최적화 도구였다.
이로써 useCallback과 useMemo라는 최적화 훅 듀오가 완성되었다.
useCallback(fn, deps)은 함수(fn) 자체를 기억한다.useMemo(() => value, deps)는 함수 실행의 결과(value)를 기억한다.
사실, useCallback은 useMemo의 문법적 설탕(syntactic sugar)에 가까웠다.
useCallback(fn, deps)는 useMemo(() => fn, deps)와 완전히 동일하게 작동했다.
이 두 개의 훅은 개발자들에게 더 세밀한 성능 제어 능력을 부여했다.
하지만 동시에, 이 강력한 힘에는 책임이 따랐다.
모든 함수와 모든 값을 useCallback과 useMemo로 감싸는 것은, 오히려 코드를 복잡하게 만들고 새로운 메모리를 사용하는 또 다른 종류의 비효율을 낳을 수 있었다.
리액트 팀은 이 새로운 도구들을 남용해서는 안 된다는, 중요한 경고 메시지를 함께 전달해야 함을 알고 있었다. 최적화는 공짜가 아니었다.


