useMemo()는 어떻게 동작할까

DongHyun Park·2024년 10월 7일
1

React

목록 보기
3/6
post-thumbnail

React 개발자로서 성능 최적화는 항상 중요한 주제입니다. 그 중에서도 useMemo는 계산 비용이 높은 연산의 결과를 메모이제이션하는 강력한 도구입니다. 이번 글에서는 useMemo의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보고, React.memo와의 비교 및 최적화 전략까지 알아보겠습니다.

목차

  1. useMemo란?
  2. React의 소스 코드 구조
  3. useMemo의 공개 API
  4. useMemo의 내부 구현
  5. 의존성 배열의 동작
  6. useMemo와 렌더링 사이클
  7. useMemo의 한계와 주의점
  8. useMemo vs React.memo
  9. 성능 최적화 전략
  10. useMemo vs useCallback
  11. 결론

useMemo란?

useMemo는 계산 비용이 높은 함수의 결과값을 메모이제이션하는 React Hook입니다. 이를 통해 불필요한 재계산을 방지하고, 컴포넌트의 성능을 최적화할 수 있습니다.

React의 소스 코드 구조

React의 소스 코드는 여러 패키지로 구성되어 있습니다. useMemo와 관련된 주요 파일들은 다음과 같습니다:

  1. packages/react/src/ReactHooks.js: Hooks의 공개 API를 정의합니다.
  2. packages/react-reconciler/src/ReactFiberHooks.js: Hooks의 실제 구현을 담당합니다.

useMemo의 공개 API

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);
}

이 코드는 다음과 같은 중요한 점들을 보여줍니다:

  1. useMemocreate 함수와 deps 배열을 인자로 받습니다.
  2. 실제 구현은 dispatcher.useMemo로 위임됩니다.
  3. resolveDispatcher 함수는 현재 React의 렌더링 단계에 따라 적절한 dispatcher를 반환합니다.

useMemo의 내부 구현

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;
}

여기서 우리는 두 가지 주요 함수를 볼 수 있습니다:

  1. mountMemo: 컴포넌트가 처음 마운트될 때 호출됩니다.
  2. 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의 한계와 주의점

  1. 과도한 사용: 모든 값에 useMemo를 사용하는 것은 오히려 성능을 저하시킬 수 있습니다.
  2. 메모리 사용: 메모이제이션된 값은 메모리를 차지하므로, 적절한 사용이 필요합니다.
  3. 의존성 배열 관리: 잘못된 의존성 배열 설정은 버그를 유발할 수 있습니다.

useMemo vs React.memo

useMemoReact.memo는 모두 최적화를 위한 도구이지만, 그 적용 대상과 방식에 차이가 있습니다.

React.memo 복습

이전에 작성한 React.memo에 대한 분석을 참고하면, React.memo는 컴포넌트 레벨에서 작동하는 반면, useMemo는 값 레벨에서 작동합니다.

const MyComponent = React.memo(({ user }) => {
  // 렌더링 로직
}, (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id;
});

useMemo와 React.memo의 올바른 사용

// 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>;
};
  • React.memo: 컴포넌트의 props가 변경되지 않으면 전체 컴포넌트의 리렌더링을 방지합니다.
  • useMemo: 컴포넌트 내부에서 특정 값의 계산만을 최적화합니다.

성능 최적화 전략

  1. 복잡한 계산에만 사용: 계산 비용이 높은 연산에만 useMemo를 사용합니다.

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  2. 객체 생성 최적화: 렌더링마다 새로운 객체를 생성하는 경우, useMemo를 사용하여 최적화할 수 있습니다.

    const memoizedObject = useMemo(() => ({ x, y }), [x, y]);
  3. 컴포넌트 리렌더링 방지: React.memo를 사용하여 불필요한 리렌더링을 방지하고, 필요한 경우에만 내부적으로 useMemo를 사용합니다.

    const MemoizedComponent = React.memo(({ value, otherProp }) => {
      const expensiveValue = useMemo(() => expensiveComputation(value), [value]);
      return <div>{expensiveValue} {otherProp}</div>;
    });
  4. 의존성 배열 주의: useMemo의 의존성 배열을 신중히 관리하여 불필요한 재계산이나 버그를 방지합니다.

useMemo vs useCallback

useMemouseCallback은 비슷한 목적으로 사용되지만, 약간의 차이가 있습니다:

  • 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의 지속적인 발전과 함께, 우리도 이러한 최적화 기법들을 계속해서 학습하고 적용해 나가야 할 것입니다.

0개의 댓글