memo는 props가 지난번과 같을 때 컴포넌트가 리렌더링을 건너뛰게 합니다. 컴포넌트를 memo(...)로 감싸면, 부모가 렌더링될 때 React가 먼저 새 prop을 각각 이전 것과 비교하고, 모두 같으면 컴포넌트를 다시 호출하는 대신 이전 결과를 재사용합니다.
input에 입력해 보세요. 입력이 밀리지 않고, Console에는 첫 렌더링에서 나온 render SlowList가 한 번만 보입니다. memo( 래퍼(와 닫는 괄호)를 지우고 다시 입력하면, 이제 키를 누를 때마다 목록 때문에 150ms를 기다리고 렌더링 로그가 남습니다.
문법
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
SomeComponent는 어떤 함수 컴포넌트든 됩니다. 보통은 정의를 바로 감쌉니다:const Card = memo(function Card(props) { ... }). 안쪽 함수에 이름을 붙이면 React DevTools와 오류 메시지에 이름이 남습니다.arePropsEqual은 선택 사항이며 거의 필요 없습니다(아래 참고).- 결과는 다른 컴포넌트처럼 렌더링하는 컴포넌트입니다.
기본적으로 React는 이전 props와 새 props를 얕게 비교합니다. 각 prop을 Object.is로 확인합니다. 문자열, 숫자, 불리언은 값으로 비교하므로 title="Hi"는 title="Hi"와 같습니다. 객체, 배열, 함수는 참조로 비교합니다. 내용이 같은 두 객체도 여전히 다른 객체입니다.
위 예제에서 items는 컴포넌트 바깥에 선언되어 있어 렌더링마다 같은 배열이므로 비교를 통과합니다.
새 객체와 함수는 memo를 무력화합니다
부모 본문 안에 리터럴로 쓴 것은 무엇이든 렌더링마다 다시 만들어집니다: { name: 'Ada' }, [1, 2, 3], () => save(). 이것을 memo 컴포넌트에 넘기면 prop은 절대 이전 것과 같지 않습니다.
클릭할 때마다 render Badge object prop이 기록되지만 문자열 버전은 절대 기록되지 않습니다. 객체의 경우를 고치는 방법은 세 가지입니다.
- 원시 값을 넘기세요. 자식에 필드 한두 개만 필요하다면
user={{ name: 'Ada' }}대신name="Ada"를 넘기세요. - 상수를 컴포넌트 밖으로 옮기세요. 절대 바뀌지 않는 객체는 첫 번째 예제의
items처럼 모듈 최상위에 둘 수 있습니다. - 캐시하세요. props나 state로 만든 객체와 배열은 useMemo로, 함수는 useCallback으로 감싸세요.
children에도 같은 문제가 있습니다. <Card><p>Hello</p></Card>는 렌더링마다 새 <p> 요소 객체를 만들므로, children을 받는 memo Card는 매번 다시 렌더링됩니다.
memo가 막지 못하는 것
memo는 부모에게서 오는 props만 봅니다. 메모이제이션된 컴포넌트도 다음 경우에는 렌더링됩니다.
- 자기 state가 바뀔 때: 자기
useState나useReducer를 통해서입니다. - 읽는 컨텍스트가 바뀔 때:
useContext를 통해서입니다.
Unrelated를 클릭하면 Panel은 건너뜁니다. 테마를 바꾸면 컨텍스트를 읽기 때문에 렌더링됩니다. Like를 클릭하면 자기 state가 바뀌었으므로 렌더링됩니다. 의도된 동작입니다. memo는 컴포넌트가 의존하는 것이 아무것도 바뀌지 않았을 때 작업을 건너뛰기 위한 것이고, 컨텍스트와 state는 컴포넌트가 의존하는 것입니다.
커스텀 비교 함수
두 번째 인자는 얕은 비교를 대체합니다. 이전 props와 다음 props를 받아서, 같은 것으로 취급해야 할 때 true를 반환합니다. 그러면 React가 렌더링을 건너뜁니다.
const MemoChart = memo(Chart, (prev, next) => {
return (
prev.points.length === next.points.length &&
prev.points.every((p, i) => p === next.points[i]) &&
prev.color === next.color &&
prev.onSelect === next.onSelect
);
});
이것은 거의 필요 없고, 틀리기 쉽습니다. 함수가 어떤 prop을 무시하면 컴포넌트는 그 prop의 이전 값을 계속 보여 줍니다. 함수 prop을 무시하는 것이 최악입니다. 건너뛴 컴포넌트가 이전 state를 읽는 이전 함수를 계속 호출하기 때문입니다. 모든 prop을 비교하고, 큰 데이터에 깊은 비교를 하지 마세요. 그러면 비교가 아끼는 렌더링보다 더 비싸집니다. useMemo와 useCallback으로 props를 안정적으로 만드는 것이 거의 항상 더 나은 해결책입니다.
memo가 동작하는지 확인하기
가장 빠른 확인 방법은 이 페이지에서 쓴 것입니다. 컴포넌트 맨 위에 console.log를 두는 것입니다. 건너뛸 것으로 예상했는데 출력된다면 props도 로그로 남기고 이전 것과 비교해서 바뀐 것을 찾으세요. 실제 프로젝트에서는 React DevTools가 더 도와줍니다. 설정에서 "Highlight updates when components render"를 켜면 상호작용마다 어떤 컴포넌트가 깜빡이는지 볼 수 있고, Profiler 탭은 모든 렌더링을 기록하며, 해당 설정을 켜면 각 컴포넌트가 왜 렌더링되었는지도 보여 줍니다.
memo가 실패한 것처럼 보이지만 그렇지 않은 경우가 두 가지 있습니다.
- 첫 렌더링은 항상 일어납니다.
memo는 컴포넌트가 그 props로 한 번 렌더링된 뒤에만 렌더링을 건너뜁니다. - 개발 빌드에서는 더 많이 렌더링될 수 있습니다. StrictMode에서 React는 순수하지 않은 코드를 잡기 위해 개발 환경에서 각 컴포넌트를 두 번 렌더링합니다. 여기 미리보기는 프로덕션 빌드처럼 실행되므로, 위의 각 로그 줄은 실제 렌더링 한 번을 뜻합니다.
memo는 언제 쓸까
memo는 컴포넌트의 렌더링이 느리고 부모가 그 컴포넌트에 같은 props를 주며 자주 렌더링될 때 가치가 있습니다. input 옆의 긴 목록이나 차트, 매초 업데이트되는 페이지 안의 사이드바 같은 것들입니다. 빠르게 렌더링되는 컴포넌트나 props가 어차피 매번 바뀌는 컴포넌트에는 비교만 늘 뿐 얻는 것이 없습니다. 메모이제이션하기 전에 렌더링 페이지에서 보여 주듯 state를 아래로 옮기거나 JSX를 children으로 넘겨서 구조를 바꿀 수 있는지 먼저 확인하세요.
프로젝트에서 React Compiler 1.0을 쓴다면 빌드 시점에 컴포넌트와 그 props를 자동으로 메모이제이션하므로, 직접 쓰는 memo 호출 대부분이 필요 없어집니다. 컴파일러가 없다면 memo는 여기서 설명한 대로 정확히 동작하며 React 코드에서 여전히 흔합니다.
자주 묻는 질문
React.memo는 무엇을 하나요?
메모이제이션된 버전의 컴포넌트를 반환합니다. 부모가 렌더링될 때 React는 새 props를 이전 props와 비교하고, 모든 prop이 Object.is 기준으로 같으면 컴포넌트의 렌더링을 건너뜁니다.
memo 컴포넌트가 여전히 다시 렌더링되는 이유는 무엇인가요?
보통 prop 중 하나가 부모의 렌더링 중에 만든 새 객체, 배열, 함수여서 이전 것과 절대 같지 않기 때문입니다. 또한 컴포넌트는 memo와 상관없이 자기 state가 바뀌거나 읽는 컨텍스트가 바뀔 때도 렌더링됩니다.
React.memo와 useMemo의 차이는 무엇인가요?
memo는 컴포넌트를 감싸서 그 렌더링을 건너뜁니다. useMemo는 컴포넌트 안에서 계산된 값을 캐시하는 훅입니다. 둘은 자주 함께 쓰입니다. useMemo가 객체 prop을 안정적으로 유지해서 memo가 건너뛸 수 있게 합니다.
모든 컴포넌트를 memo로 감싸야 하나요?
아니요. 대부분의 렌더링은 비용이 적고, props 비교에도 자기 비용이 있습니다. 렌더링이 느리고 같은 props를 자주 받는 컴포넌트에 쓰세요.
memo는 children과 함께 동작하나요?
거의 아닙니다. children으로 넘긴 JSX는 렌더링마다 새 객체이므로, children을 받는 memo 컴포넌트는 거의 항상 다시 렌더링됩니다.