memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 재렌더링을 생략할 수 있음.
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
컴포넌트를 memo로 감싸면 해당 컴포넌트의 memoize된 버전을 얻을 수 있음. Memoize된 컴포넌트 버전은 일반적으로 부모 컴포넌트가 다시 렌더링될 때 props가 변경되지 않는 한 다시 렌더링되지 않음. memoization는 성능 최적화를 위한 것이지 성능 최적화를 보장하는 것은 아니므로 ?React는 다시 렌더링할 수도 있음?.
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});
Component: memoize하려는 컴포넌트. memo는 이 컴포넌트를 수정하지 않고 대신 memoize된 새 컴포넌트를 반환함. 함수 및 forwardRef 컴포넌트를 포함한 모든 유효한 React 컴포넌트를 사용할 수 있음.
arePropsEqual (optional): 컴포넌트의 이전 props와 새 props, 두 가지 인수를 받는 함수. 이전 props와 새 props가 같으면, 즉, 컴포넌트가 새 props를 가지고도 이전 props와 동일한 결과를 렌더링하고 동일한 방식으로 작동하면, true를 반환함. 그렇지 않으면 false를 반환함. 보통 이 함수를 지정하지 않고, React는 기본적으로 각 prop을 Object.is로 비교함.
새로운 React 컴포넌트를 반환함. 이 컴포넌트는 memo에 제공된 컴포넌트와 동일하게 동작하지만, 부모 컴포넌트가 다시 렌더링될 때 props가 변경되지 않는 한 ?React가 항상 다시 렌더링하지 않는다?는 점이 다름.
React는 일반적으로 부모 컴포넌트가 다시 렌더링될 때마다 컴포넌트를 다시 렌더링함. memo를 사용하면, 새로운 props가 이전 props와 동일하다면 부모가 다시 렌더링할 때 React가 다시 렌더링하지 않는 컴포넌트를 만들 수 있음. 이러한 컴포넌트를 memoize된 컴포넌트라고 함.
컴포넌트를 memoize하려면 컴포넌트를 memo로 감싸고 memo가 반환하는 값을 원래 컴포넌트 대신 사용하면 됨:
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
React 컴포넌트는 항상 순수한 렌더링 로직을 가져야함. 즉, props, state, context가 변경되지 않은 경우 동일한 결과를 반환해야함. memo를 사용하면 컴포넌트가 이 요건을 준수한다고 React에 알리는 것이므로, props가 변경되지 않는 한 React는 다시 렌더링할 필요가 없음. memo를 사용하더라도 컴포넌트 자체의 state가 변경되거나 사용 중인 context가 변경되면 컴포넌트는 다시 렌더링됨.
아래 예제에서 Greeting 컴포넌트는 name이 변경될 때마다 다시 렌더링되지만(name이 props중 하나이기 때문에) address가 변경될 때는 렌더링하지 않습니다(address는 prop으로 Greeting에 전달되지 않았기 때문에):
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>;
});
Note
memo는 성능 최적화를 위한 용도로만 사용해야함.memo없이 코드가 작동하지 않는다면 근본적인 문제를 찾아서 먼저 해결할 것. 그런 다음memo를 추가하여 성능을 개선할 수 있음.
컴포넌트가 memoize 되어 있어도 자체 state가 변경되면 다시 렌더링됨. Memoization은 부모로부터 컴포넌트로 전달된 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>
</>
);
}
state 변수를 현재 값으로 설정하면 memo 없이도 컴포넌트를 다시 렌더링하는 것을 건너뜀. 컴포넌트 함수가 한 번 더 호출되는 것을 볼 수 있지만 결과는 버려짐.
컴포넌트가 memoize 되어 있어도 사용 중인 context가 변경되면 다시 렌더링됩니다. Memoization은 부모로부터 컴포넌트로 전달된 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.Provider value={theme}>
<button onClick={handleClick}>
Switch theme
</button>
<Greeting name="Taylor" />
</ThemeContext.Provider>
);
}
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>
);
});
'일부' context가 변경될 때만 컴포넌트가 다시 렌더링되도록 하려면 컴포넌트를 둘로 분할할 것. 외부 컴포넌트에서 context로부터 필요한 내용을 읽고 memo한 자식에게 props으로 전달하면 됨.
memo를 사용하면 컴포넌트는 어떤 prop이 이전과 'shallowly equal'하지 않을 때마다 다시 렌더링함. 즉, React는 [Object.is](shallowly equal) 비교를 사용해 컴포넌트의 모든 prop을 이전 값과 비교함. 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 }) {
// ...
});
Memoize 된 컴포넌트에 함수를 전달해야 하는 경우, 컴포넌트 외부에서 함수를 선언하여 변경되지 않도록 하거나, useCallback을 사용하여 다시 렌더링할 때마다 정의를 캐싱할 것.
드물지만 memoize 된 컴포넌트의 props 변경을 최소화하는 것이 불가능할 수도 있음. 이 경우, 커스텀 comparison 함수를 제공하면 React가 shallow equality 대신 이전과 새 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 패널을 사용하여 comparison 함수가 실제로 컴포넌트를 다시 렌더링하는 것보다 빠른지 확인해볼 것. 놀랄 수도 있음!
성능 측정을 할 때는 React가 프로덕션 모드에서 실행되고 있는지 확인해야함.
Pitfall
커스텀
arePropsEqual구현을 제공하는 경우 함수를 포함한 모든 prop을 비교해야함. 함수는 종종 부모 컴포넌트의 props와 state를 close over함.oldProps.onClick !== newProps.onClick일 때true를 반환하면 컴포넌트가 onClick 핸들러 내에서 이전 렌더링의 props와 state를 계속 '보고' 있어 매우 혼란스러운 버그가 발생할 수 있음.작업 중인 데이터 구조의 깊이가 제한되어 있다는 것이 100% 확실하지 않다면
arePropsEqual내부에서 깊은 동일성 검사를 수행하지 말 것. 깊은 동일성 검사는 매우 느려질 수 있으며 나중에 누군가 데이터 구조를 변경하면 앱이 몇 초 동안 정지될 수 있음.