앞 절의 실행 방법처럼 두 파일은 각각 따로 실행합니다. Counter.js를 선택할 때 App.jsx는 ParentComponentMemoDemo를 가져와 배치하고, MemoAndCallbackExample.js를 선택할 때는 ParentComponentOptimized를 가져와 배치하세요. 파일의 기본 내보내기는 import할 때 붙인 이름과 연결되지만, 경로와 선택한 예제는 함께 바꿔야 합니다. 각 실험은 새로 고쳐 초기 상태에서 시작합니다.
여기서 참조를 비교한다는 말은 자바스크립트에서 두 값이 같은 객체·배열·함수인지를 판정한다는 뜻입니다. 실제 숫자로 된 메모리 주소를 읽거나 객체 내부의 모든 속성을 자동으로 대조하는 것은 아닙니다. 그래서 같은 { a: 1 } 표현을 두 번 실행해도 서로 다른 객체가 됩니다.
앞 절에서 다루었던 예시를 다시 한번 살펴보고, React.memo의 효과를 명확하게 이해해 봅시다.
src/components/Counter.js
import React, { useState } from 'react';// 🌟 React.memo로 감싸진 자식 컴포넌트const MemoizedDisplay = React.memo(({ title, value, onIncrement }) => { console.log(`MemoizedDisplay [${title}] 렌더링됨`); // 렌더링 시점에 로그 출력 return ( <div style={{ padding: '10px', margin: '5px', border: '1px solid lightblue', borderRadius: '4px' }}> <h3>{title}</h3> <p>Value: {value}</p> {onIncrement && <button onClick={onIncrement} className="button secondary">Increment Memoized Child</button>} </div> );});// React.memo로 감싸지지 않은 일반 자식 컴포넌트const RegularDisplay = ({ title, value }) => { console.log(`RegularDisplay [${title}] 렌더링됨`); // 렌더링 시점에 로그 출력 return ( <div style={{ padding: '10px', margin: '5px', border: '1px solid lightcoral', borderRadius: '4px' }}> <h3>{title}</h3> <p>Value: {value}</p> </div> );};function ParentComponentMemoDemo() { const [appClicks, setAppClicks] = useState(0); // 앱 전체 클릭 수 (다른 상태 변화) const [memoizedValue, setMemoizedValue] = useState(0); // MemoizedDisplay에 전달될 값 // ParentComponent가 리렌더링될 때마다 이 함수는 재생성됨 (새로운 참조) const handleMemoizedIncrement = () => { setMemoizedValue(prev => prev + 1); }; return ( <div style={{ maxWidth: '600px', margin: '30px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.05)', backgroundColor: '#fff' }}> <h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '30px' }}>`React.memo` 활용 예시</h2> <p>App Clicks (Parent State): {appClicks}</p> <button onClick={() => setAppClicks(appClicks + 1)} className="button"> App Clicks 증가 (부모 리렌더링) </button> <hr style={{ margin: '20px 0' }}/> {/* 1. MemoizedDisplay: appClicks 변화로 부모 리렌더링 시, memoizedValue가 변경되지 않으면 리렌더링 안 함 */} {/* 단, onIncrement 함수가 매번 재생성되므로, 이 경우 MemoizedDisplay는 계속 리렌더링됨! */} <MemoizedDisplay title="Memoized Child (with regular function prop)" value={memoizedValue} onIncrement={handleMemoizedIncrement} /> {/* 2. RegularDisplay: 부모 리렌더링 시 항상 리렌더링 */} <RegularDisplay title="Regular Child" value={appClicks} /> <p style={{ marginTop: '20px', fontSize: '0.9em', color: '#555' }}> `App Clicks 증가` 버튼을 클릭하면서 콘솔 로그를 확인해보세요.<br/> `Regular Child`는 항상 렌더링되지만, 첫 번째 `Memoized Child`는 `handleMemoizedIncrement` 함수가 매번 재생성되기 때문에 프롭스가 변경된 것으로 간주되어 계속 리렌더링됩니다.<br/> 이 문제를 해결하려면 `useCallback`이 필요합니다. </p> </div> );}export default ParentComponentMemoDemo;
OptimizedDisplay는 App Clicks와 Perform Action 양쪽에서 다시 렌더링됩니다.
App Clicks는 data 참조를, Perform Action은 value를 바꾸기 때문입니다.
onAction 프롭스는 useCallback으로 감싸져 의존성 배열이 비어 있으므로, ParentComponentOptimized가 리렌더링되더라도 동일한 함수 참조를 유지합니다.
data 프롭스는 useMemo로 감싸져 appClicks가 변경될 때만 새로운 배열이 생성되고, 따라서 OptimizedDisplay의 data 프롭스 참조는 appClicks가 변경될 때만 바뀝니다. 배열 내용이 우연히 같아도 의존성 appClicks가 바뀌면 계산이 다시 실행되어 새 배열을 만듭니다.
dataArray 계산됨 로그는 appClicks가 변경될 때마다 찍힐 것입니다. (이 부분은 useMemo가 appClicks 의존성을 가지고 있기 때문입니다.)
이 예제는 콜백 참조를 재사용하지만, 두 버튼 모두 다른 prop을 변경하므로 자식 렌더 생략을 보여 주지는 않습니다. 로그 횟수는 개발 Strict Mode 등의 환경에 따라 추가될 수 있습니다.
이 예제에서는 React.memo, useCallback, useMemo를 모두 사용해도 두 버튼이 서로 다른 prop을 바꾸므로 자식 렌더링 생략을 보여 주지 않습니다. 도구의 개수보다 실제로 어떤 입력이 달라졌는지가 판단의 기준입니다.
성능 병목 지점을 찾았을 때: React Developer Tools의 Profiler를 통해 불필요한 리렌더링이나 느린 컴포넌트를 식별했을 때.
컴포넌트의 렌더링 비용이 높을 때: 컴포넌트가 복잡한 UI를 렌더링하거나, 많은 계산을 수행하는 경우.
컴포넌트의 프롭스가 자주 변경되지 않을 때: 부모의 잦은 리렌더링에도 불구하고, 자식 컴포넌트에게 전달되는 프롭스는 대부분 동일하게 유지될 때.
Pure Component (순수 컴포넌트)처럼 동작해야 할 때: 동일한 프롭스에 대해 항상 동일한 렌더링 결과를 보장하는 컴포넌트.
언제 사용하지 않아야 하는가?
프롭스가 자주 변경되는 컴포넌트: 매번 프롭스를 비교하는 오버헤드가 리렌더링 오버헤드보다 커질 수 있습니다.
렌더링 비용이 매우 낮은 컴포넌트: 간단한 HTML 요소만 렌더링하는 컴포넌트는 최적화 효과가 미미하고 오히려 오버헤드만 추가합니다.
자신만의 상태를 자주 업데이트하는 컴포넌트: React.memo는 프롭스에만 관심이 있으며, 컴포넌트 내부 상태 변경 시에는 항상 리렌더링됩니다.
예시
첫 예제에서 콜백 때문에 기본 비교가 달라지는 조건만 바꿔 봅니다. 원 Counter.js를 보존하고 복사본 CounterStable.js를 만든 뒤, import에 useCallback을 추가하고 ParentComponentMemoDemo 안의 handleMemoizedIncrement 선언만 다음 코드로 바꿉니다. App.jsx의 import도 복사본을 가리키게 합니다.
이 함수는 이전 값을 setter의 함수형 갱신 인수로 받으므로 현재 렌더의 memoizedValue를 읽지 않습니다. 그래서 이 코드의 빈 의존성은 그 값의 변경을 빠뜨린 것이 아닙니다. 원 선언과 복사본을 비교할 때는 React Compiler 자동 최적화 여부를 고정하고, 새로 고친 뒤 App Clicks 버튼만 한 번 누릅니다. title과 value는 같고, 복사본의 onIncrement는 캐시가 유지되는 동안 같은 함수입니다. 이 조건에서 MemoizedDisplay가 부모의 렌더링에서 비롯된 자식 렌더링을 건너뛸 수 있는지 관찰합니다. 다음으로 자식의 Increment Memoized Child 버튼을 누르면 value가 바뀌므로 자식 화면은 1로 갱신되어야 합니다. 콘솔 횟수보다 먼저 이 출력이 맞는지 확인합니다.
같은 배열 내용이면 같은 prop일까?
두 번째 원 예제를 새로 고친 뒤 App Clicks를 다섯 번 누르고 Perform Action을 두 번 누릅니다. 최종 appClicks, displayValue, dataArray의 내용을 적으세요. 첫 화면의 배열 내용과 같아지는 시점에도 data prop의 참조가 같다고 결론 내릴 수 있을까요?
독립 비교에서는 value가 1인 이전 props와 다음 props를 가정합니다. 이전 onAction은 Alice를 안내하는 함수이고 다음 onAction은 Bob을 안내하는 새 함수입니다. 비교 함수가 value만 같다고 보고 true를 반환하면 어떤 동작을 놓치나요? 함수가 달라도 괜찮다는 결론을 화면의 숫자가 같다는 사실만으로 내릴 수 있는지 설명하세요.
바뀐 prop과 비교 함수의 책임을 나눈 풀이
최종 appClicks는 5, displayValue는 2입니다. 배열 길이는 5를 5로 나눈 나머지 0에 1을 더한 1이고, 내용은 Item 0 하나입니다. 첫 화면에서도 길이는 1이었지만 appClicks가 바뀔 때마다 useMemo의 의존성 비교 결과가 달라져 Array.from을 다시 실행합니다. 값이 같은 Item 0을 담았어도 새 배열이므로 기본 비교에서 같은 참조로 보지 않습니다. 마지막 두 Perform Action은 appClicks를 그대로 두고 value를 1, 2로 바꾸므로 이 동작에서도 자식의 출력이 갱신됩니다.
반면 첫 예제의 복사본에서 App Clicks만 바꾸면 자식의 title, value, onIncrement를 모두 같게 유지할 수 있습니다. 원 예제는 콜백 하나가 달라지고, 복사본은 그 조건을 바꿨다는 차이가 있습니다. 이것은 값·참조의 조건 비교이며 실제 시간 단축은 같은 시나리오를 측정해 확인해야 합니다.
독립 비교의 value는 같아도 onAction의 동작은 Alice 안내에서 Bob 안내로 달라졌습니다. 함수 prop을 빠뜨린 비교 함수가 true를 반환하고 이전 결과가 재사용되면, 화면 숫자는 같아 보이면서도 버튼은 오래된 Alice 콜백을 유지할 수 있습니다. 비교 함수가 책임져야 하는 것은 화면뿐 아니라 다음 상호작용의 동작도 같다는 조건입니다. 알려진 모든 prop을 정확히 비교하기 어렵다면 기본 비교를 유지하고 props 구조를 먼저 단순하게 만드는 편이 낫습니다.
기본 비교와 커스텀 비교의 조건은 memo 공식 설명에 정리되어 있습니다. 성능 캐시를 제거해도 기능은 맞아야 하며, 같은 state나 Context 조건까지 커스텀 비교로 무시하려고 하지 않습니다.
React.memo의 성패는 컴포넌트를 감싸는 코드 한 줄보다,
부모가 전달하는 props의 참조가 안정적으로 유지되는지에 달려 있습니다.
10장 3절 "React.memo 소개"는 여기까지입니다.
이 장에서는 React.memo의 작동 원리인 얕은 비교를 이해하고, useCallback, useMemo와 함께 사용될 때의 시너지를 실습 예제를 통해 확인했습니다.
또한, React.memo를 언제 사용해야 하고, 언제 피해야 하는지에 대한 가이드라인을 제시했습니다.
이제 컴포넌트 수준에서 불필요한 리렌더링을 줄이는 주요 도구와 적용 기준을 정리할 수 있습니다.