메모이제이션(Memoization)은 이전에 계산한 결과를 저장해 두었다가, 동일한 입력이 들어오면 재계산 없이 저장된 값을 반환하는 최적화 기법입니다. React에서는 이 개념을 컴포넌트 렌더링과 값 계산에 적용하여 불필요한 작업을 줄입니다.
React 컴포넌트는 부모가 렌더링될 때마다 자식도 함께 렌더링됩니다. 대부분의 경우 이는 빠르게 처리되지만, 자식 컴포넌트의 렌더링 비용이 크거나 컴포넌트 트리가 깊을 때는 성능 저하로 이어질 수 있습니다. React는 이 문제를 해결하기 위해 React.memo, useMemo, useCallback 세 가지 도구를 제공합니다.
React.memo는 컴포넌트를 감싸는 고차 컴포넌트(HOC)입니다. 부모가 리렌더링되더라도 자식 컴포넌트에 전달된 props가 이전과 동일하면 자식의 렌더링을 건너뜁니다.
const Child = React.memo(function Child({ value }) {
return <div>{value}</div>;
});
기본적으로 props 비교는 얕은 비교(shallow comparison)로 수행됩니다. 객체나 배열처럼 참조 타입의 props는 매 렌더링마다 새로운 참조가 생성되므로, React.memo를 적용해도 효과가 없는 경우가 많습니다. 두 번째 인자로 커스텀 비교 함수를 전달하면 이를 제어할 수 있습니다.
얕은 비교(Shallow Comparison): 객체의 내부 값 대신 참조(메모리 주소)를 비교하는 방식.
useMemo는 계산 비용이 큰 값을 메모이제이션합니다. 의존성 배열의 값이 변경될 때만 재계산이 실행됩니다.
const sorted = useMemo(() => {
return [...list].sort((a, b) => a.score - b.score);
}, [list]);
필터링, 정렬, 집계처럼 반복적으로 호출될 수 있는 무거운 연산에 적합합니다. 단순한 값 계산에 적용하면 메모이제이션 자체의 비용이 오히려 더 클 수 있습니다.
useCallback은 함수를 메모이제이션합니다. 의존성 배열이 변경되지 않으면 동일한 함수 참조를 반환합니다.
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
함수를 props로 자식 컴포넌트에 전달할 때 유용합니다. React.memo로 감싼 자식에게 함수를 내려보내는 경우, useCallback 없이는 매 렌더링마다 새 함수가 생성되어 자식이 항상 리렌더링됩니다.
+------------------+ +------------------+ +------------------+
| React.memo | | useMemo | | useCallback |
| | | | | |
| 컴포넌트 자체를 | | 계산된 값을 | | 함수 참조를 |
| 캐싱 | | 캐싱 | | 캐싱 |
+------------------+ +------------------+ +------------------+
컴포넌트 값 함수
세 도구는 서로 보완적으로 작동합니다. React.memo로 자식을 보호하고, useCallback으로 함수 props의 참조 안정성을 확보하며, useMemo로 무거운 계산 결과를 재사용합니다.
메모이제이션은 공짜가 아닙니다. 이전 값을 저장하고 의존성을 비교하는 작업에도 CPU와 메모리 비용이 소요됩니다.
다음과 같은 상황에서는 메모이제이션이 오히려 역효과를 냅니다.
useMemo를 적용하는 경우 — 비교 비용이 계산 비용보다 클 수 있습니다.React.memo의 비교 작업이 렌더링 자체보다 비쌀 수 있습니다.최적화는 측정이 먼저입니다. React DevTools의 Profiler를 사용해 실제로 병목이 되는 지점을 확인한 후 적용하는 것이 올바른 순서입니다. 막연하게 모든 컴포넌트에 React.memo를 적용하거나 모든 함수를 useCallback으로 감싸는 것은 코드 복잡도만 높이고 성능 개선 효과는 미미할 수 있습니다.