React.memo()는 어떻게 동작할까

DongHyun Park·2024년 9월 19일
2

React

목록 보기
1/6
post-thumbnail

React 개발자로서 성능 최적화는 항상 중요한 주제입니다. 그 중에서도 React.memo()는 컴포넌트 렌더링 최적화를 위한 강력한 도구입니다. 이번 글에서는 React.memo()의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보겠습니다.

목차

  1. React.memo()란?
  2. 소스 코드 위치
  3. memo 함수의 구조
  4. 비교 함수의 동작
  5. 메모이제이션의 실제 구현
  6. React.memo()의 한계와 주의점
  7. 성능 최적화 팁
  8. 결론

React.memo()란?

React.memo()는 고차 컴포넌트(Higher Order Component)로, 컴포넌트의 props가 변경되지 않았을 때 리렌더링을 방지합니다. 이는 특히 부모 컴포넌트가 자주 리렌더링되는 큰 애플리케이션에서 성능을 크게 향상시킬 수 있습니다.

소스 코드 위치

React의 소스 코드는 GitHub에서 공개되어 있습니다. React.memo()의 구현은 다음 파일에서 찾을 수 있습니다:

  • packages/react/src/ReactMemo.js

    packages/react/~ : React의 공개 API를 정의 및 구현한 위치

    • 포함된 기능
      - React 요소 생성(createElement)
      - 컴포넌트 정의(Component, PureComponent)
      - Hooks(useState, useEffect 등)
      - ContextAPI
      - React.memo, React.lazy 등의 최적화 기능
  • packages/react-reconciler/src/ReactFiberBeginWork.js

    packages/react-reconiler/~ : React의 내부 동작을 담당하며, Renderer(ReactDOM, React Native 등)와 연결되어 실제 UI 업데이트를 조정

    • 포함된 기능
      - 가상 DOM 구현
      - Fiber 아키텍처 (비동기 렌더링을 가능하게 하는 내부 객체 모델)
      - 컴포넌트 생명주기 관리
      - 상태 업데이트 처리
      - 렌더링 우선순위 관리

memo 함수의 구조

먼저 packages/react/src/ReactMemo.js 파일을 살펴보겠습니다:

export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  // ... Dev 코드 ...
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
  return elementType;
}

이 코드에서 우리는 몇 가지 중요한 점을 발견할 수 있습니다:

  1. memo 함수는 컴포넌트 타입(type)과 선택적인 비교 함수(compare)를 인자로 받습니다.
  2. 반환값은 REACT_MEMO_TYPE이라는 특별한 $$typeof 속성을 가진 객체입니다.
  3. 비교 함수가 제공되지 않으면 null로 설정됩니다.

비교 함수의 동작

비교 함수는 이전 props와 새로운 props를 비교하여 컴포넌트를 리렌더링할지 결정합니다. 기본적으로 React는 얕은 비교(shallow comparison)를 수행합니다.

packages/react-reconciler/src/ReactFiberBeginWork.js에서 관련 코드를 찾을 수 있습니다:

function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  updateLanes: Lanes,
): null | Fiber {
  // ...

  if (current !== null) {
    const prevProps = current.memoizedProps;
    if (
      shallowEqual(prevProps, nextProps) &&
      current.ref === workInProgress.ref &&
      workInProgress.type === current.type
    ) {
      didReceiveUpdate = false;
      // ...
    }
  }

  // ...
}

이 코드에서 shallowEqual 함수를 사용하여 이전 props와 새 props를 비교하는 것을 볼 수 있습니다. 만약 props가 같고 ref와 type도 변경되지 않았다면, didReceiveUpdatefalse로 설정하여 리렌더링을 방지합니다.

메모이제이션의 실제 구현

React는 메모이제이션된 결과를 Fiber 노드의 memoizedPropsmemoizedState에 저장합니다. 이 값들은 다음 렌더링 사이클에서 재사용됩니다.

if (didReceiveUpdate) {
  // ...
} else {
  // 메모이제이션된 결과를 재사용
  workInProgress.memoizedProps = nextProps;
  workInProgress.memoizedState = currentState;
}

이 최적화로 인해 불필요한 렌더링 계산을 건너뛸 수 있어 성능이 향상됩니다.

React.memo()의 한계와 주의점

  1. 얕은 비교의 한계: 기본적으로 얕은 비교만 수행하므로, 복잡한 객체나 배열의 변경을 감지하지 못할 수 있습니다.

  2. 함수 props: 매 렌더링마다 새로운 함수가 생성되면 메모이제이션의 이점을 얻기 어려울 수 있습니다.

  3. 과도한 사용: 모든 컴포넌트에 React.memo()를 적용하는 것은 오히려 성능을 저하시킬 수 있습니다.

성능 최적화 팁

  1. 커스텀 비교 함수 사용: 복잡한 props 구조에 대해서는 커스텀 비교 함수를 제공하는게 좋습니다.

    const MyComponent = React.memo(({user}) => {
      // 렌더링 로직
    }, (prevProps, nextProps) => {
      return prevProps.user.id === nextProps.user.id;
    });
  2. 불변성 유지: props로 전달되는 객체나 배열의 불변성을 유지하면 얕은 비교만으로도 효과적인 최적화가 가능합니다.

  3. useCallback과 함께 사용: 함수 props를 전달할 때는 useCallback을 사용하여 함수를 메모이제이션하는게 좋습니다.

    const memoizedCallback = useCallback(() => {
      doSomething(a, b);
    }, [a, b]);

결론

React.memo()는 강력한 성능 최적화 도구이지만, 그 내부 동작을 이해하고 적절히 사용하는 것이 중요합니다. 이 글에서 살펴본 것처럼, React의 내부 구현은 복잡하지만 효율적으로 설계되어 있습니다.

개발자로서 우리는 이러한 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞게 적절히 활용해야 합니다. React.memo()를 통한 최적화는 큰 애플리케이션에서 눈에 띄는 성능 향상을 가져올 수 있지만, 항상 측정과 프로파일링을 통해 그 효과를 검증해야 합니다.

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

0개의 댓글