React 개발자로서 성능 최적화는 항상 중요한 주제입니다. 그 중에서도 React.memo()
는 컴포넌트 렌더링 최적화를 위한 강력한 도구입니다. 이번 글에서는 React.memo()
의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보겠습니다.
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 아키텍처 (비동기 렌더링을 가능하게 하는 내부 객체 모델)
- 컴포넌트 생명주기 관리
- 상태 업데이트 처리
- 렌더링 우선순위 관리
먼저 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;
}
이 코드에서 우리는 몇 가지 중요한 점을 발견할 수 있습니다:
memo
함수는 컴포넌트 타입(type
)과 선택적인 비교 함수(compare
)를 인자로 받습니다.REACT_MEMO_TYPE
이라는 특별한 $$typeof 속성을 가진 객체입니다.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도 변경되지 않았다면, didReceiveUpdate
를 false
로 설정하여 리렌더링을 방지합니다.
React는 메모이제이션된 결과를 Fiber
노드의 memoizedProps
와 memoizedState
에 저장합니다. 이 값들은 다음 렌더링 사이클에서 재사용됩니다.
if (didReceiveUpdate) {
// ...
} else {
// 메모이제이션된 결과를 재사용
workInProgress.memoizedProps = nextProps;
workInProgress.memoizedState = currentState;
}
이 최적화로 인해 불필요한 렌더링 계산을 건너뛸 수 있어 성능이 향상됩니다.
얕은 비교의 한계: 기본적으로 얕은 비교만 수행하므로, 복잡한 객체나 배열의 변경을 감지하지 못할 수 있습니다.
함수 props: 매 렌더링마다 새로운 함수가 생성되면 메모이제이션의 이점을 얻기 어려울 수 있습니다.
과도한 사용: 모든 컴포넌트에 React.memo()
를 적용하는 것은 오히려 성능을 저하시킬 수 있습니다.
커스텀 비교 함수 사용: 복잡한 props 구조에 대해서는 커스텀 비교 함수를 제공하는게 좋습니다.
const MyComponent = React.memo(({user}) => {
// 렌더링 로직
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});
불변성 유지: props로 전달되는 객체나 배열의 불변성을 유지하면 얕은 비교만으로도 효과적인 최적화가 가능합니다.
useCallback과 함께 사용: 함수 props를 전달할 때는 useCallback
을 사용하여 함수를 메모이제이션하는게 좋습니다.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
React.memo()
는 강력한 성능 최적화 도구이지만, 그 내부 동작을 이해하고 적절히 사용하는 것이 중요합니다. 이 글에서 살펴본 것처럼, React의 내부 구현은 복잡하지만 효율적으로 설계되어 있습니다.
개발자로서 우리는 이러한 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞게 적절히 활용해야 합니다. React.memo()
를 통한 최적화는 큰 애플리케이션에서 눈에 띄는 성능 향상을 가져올 수 있지만, 항상 측정과 프로파일링을 통해 그 효과를 검증해야 합니다.
React의 지속적인 발전과 함께, 우리도 이러한 최적화 기법들을 계속해서 학습하고 적용해 나가야 할 것입니다.