memo를 사용하면 컴포넌트의 props가 변경되지 않았을 때 리렌더링을 건너뛸 수 있어요.
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
참고
React Compiler는 모든 컴포넌트에
memo와 동일한 효과를 자동으로 적용해서, 수동으로 메모이제이션할 필요를 줄여줘요. 컴파일러를 사용하면 컴포넌트 메모이제이션을 자동으로 처리할 수 있어요.
memo(Component, arePropsEqual?)컴포넌트를 memo로 감싸면 해당 컴포넌트의 메모이제이션된 버전을 얻을 수 있어요. 이 메모이제이션된 버전의 컴포넌트는 보통 props가 변경되지 않은 한 부모 컴포넌트가 리렌더링될 때 리렌더링되지 않아요. 하지만 React가 여전히 리렌더링할 수도 있어요: 메모이제이션은 성능 최적화이지 보장이 아니거든요.
// 예시
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});
Component: 메모이제이션하고 싶은 컴포넌트예요. memo는 이 컴포넌트를 수정하지 않고, 대신 새로운 메모이제이션된 컴포넌트를 반환해요. 함수와 forwardRef 컴포넌트를 포함해서 모든 유효한 React 컴포넌트가 허용돼요.
선택적 arePropsEqual: 컴포넌트의 이전 props와 새 props 두 개의 인자를 받는 함수예요. 이전 props와 새 props가 같으면 true를 반환해야 해요: 즉, 새 props로 컴포넌트가 이전 props와 동일한 출력을 렌더링하고 동일한 방식으로 동작할 경우에요. 그렇지 않으면 false를 반환해야 해요. 보통 이 함수를 직접 지정할 필요는 없어요. 기본적으로 React는 각 prop을 Object.is로 비교해요.
memo는 새로운 React 컴포넌트를 반환해요. memo에 제공된 컴포넌트와 동일하게 동작하지만, props가 변경되지 않는 한 부모가 리렌더링될 때 React가 항상 리렌더링하지는 않아요.
React는 보통 부모가 리렌더링될 때마다 컴포넌트를 리렌더링해요. memo를 사용하면 새 props가 이전 props와 같은 한 부모가 리렌더링되어도 React가 리렌더링하지 않는 컴포넌트를 만들 수 있어요. 이런 컴포넌트를 메모이제이션되었다고 해요.
컴포넌트를 메모이제이션하려면, memo로 감싸고 반환되는 값을 원래 컴포넌트 대신 사용하세요:
// 예시
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
React 컴포넌트는 항상 순수한 렌더링 로직을 가져야 해요. 이건 props, state, context가 변경되지 않았다면 동일한 출력을 반환해야 한다는 뜻이에요. memo를 사용하면 컴포넌트가 이 요구사항을 충족한다고 React에게 알려주는 거예요. 그래서 React는 props가 변경되지 않은 한 리렌더링할 필요가 없어요. memo를 사용하더라도 컴포넌트 자체의 state가 변경되거나 사용 중인 context가 변경되면 컴포넌트는 리렌더링돼요.
이 예시에서 Greeting 컴포넌트는 name이 변경될 때마다 리렌더링되지만(props 중 하나니까요), address가 변경될 때는 리렌더링되지 않아요(Greeting에 prop으로 전달되지 않으니까요):
// 예시
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
});
label {
display: block;
margin-bottom: 16px;
}
**심층 탐구: 모든 곳에 memo를 추가해야 할까요?**참고
memo는 성능 최적화로만 사용해야 해요. 코드가memo없이 동작하지 않는다면, 먼저 근본적인 문제를 찾아서 수정하세요. 그 다음에 성능을 개선하기 위해memo를 추가할 수 있어요.
앱이 이 사이트처럼 대부분의 인터랙션이 거친(페이지나 전체 섹션을 교체하는 것처럼) 경우라면, 메모이제이션은 보통 불필요해요. 반면에 앱이 그리기 에디터처럼 대부분의 인터랙션이 세밀한(도형을 이동하는 것처럼) 경우라면, 메모이제이션이 매우 유용할 수 있어요.
memo를 사용한 최적화는 컴포넌트가 정확히 동일한 props로 자주 리렌더링되고, 리렌더링 로직이 비싼 경우에만 가치가 있어요. 컴포넌트가 리렌더링될 때 눈에 띄는 지연이 없다면, memo는 불필요해요. 컴포넌트에 전달되는 props가 항상 다른 경우, 예를 들어 렌더링 중에 정의된 객체나 일반 함수를 전달하는 경우에는 memo가 완전히 쓸모없다는 걸 명심하세요. 그래서 종종 useMemo와 useCallback을 memo와 함께 사용해야 해요.
다른 경우에는 컴포넌트를 memo로 감싸는 것에 이점이 없어요. 그렇게 해도 큰 해가 되지는 않아서, 일부 팀은 개별 경우를 생각하지 않고 가능한 한 많이 메모이제이션하는 것을 선택해요. 이 접근 방식의 단점은 코드가 덜 읽기 쉬워진다는 거예요. 또한 모든 메모이제이션이 효과적인 것은 아니에요: "항상 새로운" 단일 값만 있어도 전체 컴포넌트의 메모이제이션을 깨뜨릴 수 있어요.
실제로 몇 가지 원칙을 따르면 많은 메모이제이션을 불필요하게 만들 수 있어요:
특정 인터랙션이 여전히 느리게 느껴진다면, React Developer Tools 프로파일러를 사용해서 메모이제이션이 가장 도움이 될 컴포넌트를 찾고, 필요한 곳에 메모이제이션을 추가하세요. 이러한 원칙들은 컴포넌트를 디버깅하고 이해하기 쉽게 만들어서, 어떤 경우든 따르는 것이 좋아요. 장기적으로는 이 문제를 완전히 해결하기 위해 세밀한 메모이제이션을 자동으로 수행하는 방법을 연구하고 있어요.
컴포넌트가 메모이제이션되었더라도 자체 state가 변경되면 여전히 리렌더링돼요. 메모이제이션은 부모로부터 컴포넌트에 전달되는 props에만 관련이 있어요.
// 예시
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
const [greeting, setGreeting] = useState('Hello');
return (
<>
<h3>{greeting}{name && ', '}{name}!</h3>
<GreetingSelector value={greeting} onChange={setGreeting} />
</>
);
});
function GreetingSelector({ value, onChange }) {
return (
<>
<label>
<input
type="radio"
checked={value === 'Hello'}
onChange={e => onChange('Hello')}
/>
Regular greeting
</label>
<label>
<input
type="radio"
checked={value === 'Hello and welcome'}
onChange={e => onChange('Hello and welcome')}
/>
Enthusiastic greeting
</label>
</>
);
}
label {
display: block;
margin-bottom: 16px;
}
state 변수를 현재 값으로 설정하면, memo 없이도 React는 컴포넌트 리렌더링을 건너뛸 거예요. 컴포넌트 함수가 한 번 더 호출되는 걸 볼 수는 있지만, 결과는 버려져요.
컴포넌트가 메모이제이션되었더라도 사용 중인 context가 변경되면 여전히 리렌더링돼요. 메모이제이션은 부모로부터 컴포넌트에 전달되는 props에만 관련이 있어요.
// 예시
import { createContext, memo, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
const [theme, setTheme] = useState('dark');
function handleClick() {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
return (
<ThemeContext value={theme}>
<button onClick={handleClick}>
Switch theme
</button>
<Greeting name="Taylor" />
</ThemeContext>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
const theme = useContext(ThemeContext);
return (
<h3 className={theme}>Hello, {name}!</h3>
);
});
label {
display: block;
margin-bottom: 16px;
}
.light {
color: black;
background-color: white;
}
.dark {
color: white;
background-color: black;
}
컴포넌트가 context의 일부가 변경될 때만 리렌더링되게 하려면, 컴포넌트를 둘로 나누세요. 외부 컴포넌트에서 context에서 필요한 것을 읽고, 메모이제이션된 자식에게 prop으로 전달하세요.
memo를 사용하면 어떤 prop이든 이전 값과 얕게 동등하지 않으면 컴포넌트가 리렌더링돼요. 이건 React가 컴포넌트의 모든 prop을 Object.is 비교를 사용해서 이전 값과 비교한다는 뜻이에요. Object.is(3, 3)은 true지만, Object.is({}, {})는 false라는 점에 주목하세요.
memo를 최대한 활용하려면 props가 변경되는 횟수를 최소화하세요. 예를 들어, prop이 객체라면 useMemo를 사용해서 부모 컴포넌트가 매번 그 객체를 다시 만드는 것을 방지하세요:
// 예시
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <Profile person={person} />;
}
const Profile = memo(function Profile({ person }) {
// ...
});
props 변경을 최소화하는 더 좋은 방법은 컴포넌트가 props에서 최소한으로 필요한 정보만 받도록 하는 거예요. 예를 들어, 전체 객체 대신 개별 값을 받을 수 있어요:
// 예시
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile = memo(function Profile({ name, age }) {
// ...
});
개별 값도 때로는 덜 자주 변경되는 값으로 투영될 수 있어요. 예를 들어, 여기서 컴포넌트는 값 자체 대신 값의 존재 여부를 나타내는 boolean을 받아요:
// 예시
function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}
const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});
메모이제이션된 컴포넌트에 함수를 전달해야 할 때는, 함수를 컴포넌트 바깥에서 선언해서 절대 변경되지 않게 하거나, useCallback을 사용해서 리렌더링 사이에 정의를 캐시하세요.
드문 경우지만 메모이제이션된 컴포넌트의 props 변경을 최소화하는 것이 불가능할 수 있어요. 그런 경우 커스텀 비교 함수를 제공할 수 있는데, React는 얕은 동등성 대신 이 함수를 사용해서 이전 props와 새 props를 비교할 거예요. 이 함수는 memo의 두 번째 인자로 전달돼요. 새 props가 이전 props와 동일한 출력을 만들 때만 true를 반환해야 하고; 그렇지 않으면 false를 반환해야 해요.
// 예시
const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);
function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}
이렇게 한다면, 브라우저 개발자 도구의 Performance 패널을 사용해서 비교 함수가 실제로 컴포넌트를 리렌더링하는 것보다 빠른지 확인하세요. 놀랄 수도 있어요.
성능 측정을 할 때는 React가 프로덕션 모드에서 실행되고 있는지 확인하세요.
⚠️ 주의
커스텀
arePropsEqual구현을 제공한다면, 함수를 포함해서 모든 prop을 비교해야 해요. 함수는 종종 부모 컴포넌트의 props와 state를 클로저로 감싸요.oldProps.onClick !== newProps.onClick일 때true를 반환하면, 컴포넌트는onClick핸들러 안에서 이전 렌더링의 props와 state를 계속 "보게" 되어 매우 혼란스러운 버그가 발생해요.작업 중인 데이터 구조가 알려진 제한된 깊이를 가지고 있다고 100% 확신하지 않는 한
arePropsEqual안에서 깊은 동등성 검사를 피하세요. 깊은 동등성 검사는 엄청나게 느려질 수 있고, 누군가 나중에 데이터 구조를 변경하면 앱이 몇 초 동안 멈출 수 있어요.
React Compiler를 활성화하면, 보통 React.memo가 더 이상 필요 없어요. 컴파일러가 자동으로 컴포넌트 리렌더링을 최적화해줘요.
작동 방식은 이래요:
React Compiler 없이는 불필요한 리렌더링을 방지하기 위해 React.memo가 필요해요:
// 예시
// Parent가 매초 리렌더링돼요
function Parent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<h1>Seconds: {seconds}</h1>
<ExpensiveChild name="John" />
</>
);
}
// memo 없이는, props가 변경되지 않아도 매초 리렌더링돼요
const ExpensiveChild = memo(function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered');
return <div>Hello, {name}!</div>;
});
React Compiler가 활성화되면, 같은 최적화가 자동으로 일어나요:
// 예시
// memo가 필요 없어요 - 컴파일러가 자동으로 리렌더링을 방지해요
function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered');
return <div>Hello, {name}!</div>;
}
React Compiler가 생성하는 코드의 핵심 부분은 이래요:
// 예시
function Parent() {
const $ = _c(7);
const [seconds, setSeconds] = useState(0);
// ... 다른 코드 ...
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <ExpensiveChild name="John" />;
$[4] = t3;
} else {
t3 = $[4];
}
// ... return 문 ...
}
강조된 줄을 주목하세요: 컴파일러가 <ExpensiveChild name="John" />을 캐시 검사로 감싸요. name prop이 항상 "John"이기 때문에, 이 JSX는 한 번 생성되고 모든 부모 리렌더링에서 재사용돼요. 이건 정확히 React.memo가 하는 것과 같아요 - props가 변경되지 않았을 때 자식이 리렌더링되는 것을 방지해요.
React Compiler는 자동으로:
1. ExpensiveChild에 전달된 name prop이 변경되지 않았다는 걸 추적해요
2. 이전에 생성된 <ExpensiveChild name="John" /> JSX를 재사용해요
3. ExpensiveChild 리렌더링을 완전히 건너뛰어요
이건 React Compiler를 사용할 때 컴포넌트에서 React.memo를 안전하게 제거할 수 있다는 뜻이에요. 컴파일러가 같은 최적화를 자동으로 제공해서 코드가 더 깔끔하고 유지보수하기 쉬워져요.
참고
컴파일러의 최적화는 실제로
React.memo보다 더 포괄적이에요. 컴포넌트 내의 중간 값과 비싼 계산도 메모이제이션해서, 컴포넌트 트리 전체에React.memo와useMemo를 결합한 것과 비슷해요.
React는 얕은 동등성으로 이전 props와 새 props를 비교해요: 즉, 각 새 prop이 이전 prop과 참조가 같은지 고려해요. 부모가 리렌더링될 때마다 새 객체나 배열을 만들면, 개별 요소가 각각 같더라도 React는 변경되었다고 간주해요. 마찬가지로, 부모 컴포넌트를 렌더링할 때 새 함수를 만들면, 함수가 같은 정의를 가지더라도 React는 변경되었다고 간주할 거예요. 이걸 피하려면 props를 단순화하거나 부모 컴포넌트에서 props를 메모이제이션하세요.