리액트 컴포넌트는 기본적으로 부모 컴포넌트가 리렌더링될 때, 자신의 프롭스(props)나 상태(state)가 변경되지 않았더라도 함께 리렌더링됩니다. 이것은 리액트의 상태가 변경되면 UI를 다시 그린다는 철학에 따른 자연스러운 동작이지만, 때로는 불필요한 비용을 발생시킬 수 있습니다.
예를 들어, 매우 복잡한 계산을 수행하거나 많은 하위 컴포넌트를 렌더링하는 ExpensiveComponent가 있다고 가정해 봅시다. 이 컴포넌트가 부모로부터 받는 프롭스는 거의 변경되지 않지만, 부모의 다른 상태 변화로 인해 부모가 리렌더링될 때마다 ExpensiveComponent도 매번 리렌더링된다면, 이는 애플리케이션의 성능 저하로 이어질 수 있습니다.
React.memo는 이러한 상황에서 빛을 발합니다. React.memo는 리액트에게 이 컴포넌트는 프롭스가 변경되지 않으면 다시 렌더링할 필요가 없어라고 힌트를 주는 역할을 합니다.
OptimizedDisplay [Optimized Child] 렌더링됨 로그는 displayValue가 변경될 때만 찍힙니다. (즉, Perform Action 버튼을 클릭했을 때)
onAction 프롭스는 useCallback으로 감싸져 의존성 배열이 비어있으므로, ParentComponentOptimized가 리렌더링되더라도 동일한 함수 참조를 유지합니다.
data 프롭스는 useMemo로 감싸져 appClicks가 변경될 때만 새로운 배열이 생성되고, 따라서 OptimizedDisplay의 data 프롭스 참조는 appClicks가 변경될 때만 바뀝니다. (여기서는 appClicks % 5 + 1 로직 때문에 특정 appClicks 값에서 dataArray의 내용이 동일하면 재생성되지 않을 수 있습니다.)
dataArray 계산됨 로그는 appClicks가 변경될 때마다 찍힐 것입니다. (이 부분은 useMemo가 appClicks 의존성을 가지고 있기 때문입니다.) 하지만 OptimizedDisplay 자체는 data 프롭스의 참조가 변경될 때만 리렌더링될 것입니다.
이처럼 React.memo는 useCallback과 useMemo와 함께 사용될 때 진정한 성능 최적화 효과를 발휘합니다.
React.memo는 선택적으로 두 번째 인자로 arePropsEqual이라는 커스텀 비교 함수를 받을 수 있습니다. 이 함수는 이전 프롭스와 새로운 프롭스를 인자로 받아, 두 프롭스가 동일하다고 판단되면 true를, 다르다고 판단되면 false를 반환해야 합니다. true를 반환하면 리렌더링을 건너뛰고, false를 반환하면 리렌더링합니다.
기본적인 얕은 비교가 불충분하거나, 특정 프롭스에 대해 더 깊은 비교가 필요할 때 유용합니다. 하지만 대부분의 경우 useCallback/useMemo를 통한 참조 일관성 유지로 충분하며, 커스텀 비교 함수는 신중하게 사용해야 합니다.
const MyComponent = React.memo((props) => { // ...}, (prevProps, nextProps) => { // prevProps와 nextProps를 비교하여 리렌더링 여부 결정 // 예: 특정 객체 프롭스의 특정 속성만 비교하고 싶을 때 return prevProps.someValue === nextProps.someValue;});
성능 병목 지점을 찾았을 때: React Developer Tools의 Profiler를 통해 불필요한 리렌더링이나 느린 컴포넌트를 식별했을 때.
컴포넌트의 렌더링 비용이 높을 때: 컴포넌트가 복잡한 UI를 렌더링하거나, 많은 계산을 수행하는 경우.
컴포넌트의 프롭스가 자주 변경되지 않을 때: 부모의 잦은 리렌더링에도 불구하고, 자식 컴포넌트에게 전달되는 프롭스는 대부분 동일하게 유지될 때.
Pure Component (순수 컴포넌트)처럼 동작해야 할 때: 동일한 프롭스에 대해 항상 동일한 렌더링 결과를 보장하는 컴포넌트.
언제 사용하지 않아야 하는가?
프롭스가 자주 변경되는 컴포넌트: 매번 프롭스를 비교하는 오버헤드가 리렌더링 오버헤드보다 커질 수 있습니다.
렌더링 비용이 매우 낮은 컴포넌트: 간단한 HTML 요소만 렌더링하는 컴포넌트는 최적화 효과가 미미하고 오히려 오버헤드만 추가합니다.
자신만의 상태를 자주 업데이트하는 컴포넌트: React.memo는 프롭스에만 관심이 있으며, 컴포넌트 내부 상태 변경 시에는 항상 리렌더링됩니다.
React.memo의 성패는 컴포넌트를 감싸는 코드 한 줄보다,
부모가 전달하는 props의 참조가 안정적으로 유지되는지에 달려 있습니다.
React.memo의 필요성과 얕은 비교 동작 원리를 중심으로 정리한 보조 다이어그램입니다.
10장 3절 "React.memo 소개"는 여기까지입니다. 이 장에서는 React.memo의 작동 원리인 얕은 비교를 이해하고, useCallback, useMemo와 함께 사용될 때의 시너지를 실습 예제를 통해 확인했습니다. 또한, React.memo를 언제 사용해야 하고, 언제 피해야 하는지에 대한 가이드라인을 제시했습니다.
이제 컴포넌트 수준에서 불필요한 리렌더링을 줄이는 주요 도구와 적용 기준을 정리할 수 있습니다.