React 개발자로서 성능 최적화는 항상 중요한 주제입니다. 그 중에서도 useMemo
는 계산 비용이 높은 연산의 결과를 메모이제이션하는 강력한 도구입니다. 이번 글에서는 useMemo
의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보고, React.memo와의 비교 및 최적화 전략까지 알아보겠습니다.
useMemo
는 계산 비용이 높은 함수의 결과값을 메모이제이션하는 React Hook입니다. 이를 통해 불필요한 재계산을 방지하고, 컴포넌트의 성능을 최적화할 수 있습니다.
React의 소스 코드는 여러 패키지로 구성되어 있습니다. useMemo
와 관련된 주요 파일들은 다음과 같습니다:
packages/react/src/ReactHooks.js
: Hooks의 공개 API를 정의합니다.packages/react-reconciler/src/ReactFiberHooks.js
: Hooks의 실제 구현을 담당합니다.packages/react/src/ReactHooks.js
파일에서 useMemo
의 공개 API를 찾을 수 있습니다:
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
이 코드는 다음과 같은 중요한 점들을 보여줍니다:
useMemo
는 create
함수와 deps
배열을 인자로 받습니다.dispatcher.useMemo
로 위임됩니다.resolveDispatcher
함수는 현재 React의 렌더링 단계에 따라 적절한 dispatcher를 반환합니다.useMemo
의 실제 구현은 packages/react-reconciler/src/ReactFiberHooks.js
파일에서 찾을 수 있습니다:
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
여기서 우리는 두 가지 주요 함수를 볼 수 있습니다:
mountMemo
: 컴포넌트가 처음 마운트될 때 호출됩니다.updateMemo
: 이후 업데이트 시 호출됩니다.의존성 배열은 useMemo
의 두 번째 인자로 전달되며, 이 배열의 값들이 변경될 때만 메모이제이션된 값을 재계산합니다. React는 이전 렌더링의 의존성 값들과 현재 렌더링의 값들을 비교합니다:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
이 함수는 Object.is
를 사용하여 각 의존성을 비교합니다. 모든 의존성이 동일하면 true
를 반환하여 메모이제이션된 값을 재사용합니다.
useMemo
는 렌더링 중에 실행되며, 그 결과는 다음 렌더링까지 저장됩니다. 이는 렌더링 최적화에 도움을 주지만, 렌더링 자체를 방해하지는 않습니다.
useMemo
를 사용하는 것은 오히려 성능을 저하시킬 수 있습니다.useMemo
와 React.memo
는 모두 최적화를 위한 도구이지만, 그 적용 대상과 방식에 차이가 있습니다.
이전에 작성한 React.memo에 대한 분석을 참고하면, React.memo는 컴포넌트 레벨에서 작동하는 반면, useMemo는 값 레벨에서 작동합니다.
const MyComponent = React.memo(({ user }) => {
// 렌더링 로직
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});
// React.memo 사용
const MemoizedComponent = React.memo(({ value }) => {
const computedValue = expensiveComputation(value);
return <div>{computedValue}</div>;
});
// useMemo 사용
const MyComponent = ({ value }) => {
const memoizedValue = useMemo(() => expensiveComputation(value), [value]);
return <div>{memoizedValue}</div>;
};
복잡한 계산에만 사용: 계산 비용이 높은 연산에만 useMemo
를 사용합니다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
객체 생성 최적화: 렌더링마다 새로운 객체를 생성하는 경우, useMemo
를 사용하여 최적화할 수 있습니다.
const memoizedObject = useMemo(() => ({ x, y }), [x, y]);
컴포넌트 리렌더링 방지: React.memo
를 사용하여 불필요한 리렌더링을 방지하고, 필요한 경우에만 내부적으로 useMemo
를 사용합니다.
const MemoizedComponent = React.memo(({ value, otherProp }) => {
const expensiveValue = useMemo(() => expensiveComputation(value), [value]);
return <div>{expensiveValue} {otherProp}</div>;
});
의존성 배열 주의: useMemo
의 의존성 배열을 신중히 관리하여 불필요한 재계산이나 버그를 방지합니다.
useMemo
와 useCallback
은 비슷한 목적으로 사용되지만, 약간의 차이가 있습니다:
useMemo
: 값을 메모이제이션합니다.useCallback
: 함수를 메모이제이션합니다.// useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// useCallback
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
useCallback(fn, deps)
는 useMemo(() => fn, deps)
와 동등합니다.
useMemo
는 React 애플리케이션의 성능을 최적화하는 강력한 도구입니다. 그러나 그 내부 동작을 이해하고 적절히 사용하는 것이 중요합니다. 이 글에서 살펴본 것처럼, React의 내부 구현은 복잡하지만 효율적으로 설계되어 있습니다.
개발자로서 우리는 이러한 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞게 적절히 활용해야 합니다. useMemo
를 통한 최적화는 큰 애플리케이션에서 눈에 띄는 성능 향상을 가져올 수 있지만, 항상 측정과 프로파일링을 통해 그 효과를 검증해야 합니다.
React의 지속적인 발전과 함께, 우리도 이러한 최적화 기법들을 계속해서 학습하고 적용해 나가야 할 것입니다.