HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🐣
프론트엔드 데브코스 4기 교육생
/
😃
이동근팀
/
react@18.2.0
react@18.2.0
/
memo

memo

memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 리렌더링을 건너뛸 수 있습니다.
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • 참조
    • memo(Component, arePropsEqual?)
  • 사용법
    • prop이 변경되지 않았을 때 리렌더링 건너뛰기
    • state를 사용하여 메모화된 컴포넌트 업데이트
    • context를 사용하여 메모화된 컴포넌트 업데이트하기
    • props 변경 최소화하기
    • 사용자 정의 비교 함수 지정하기
  • 문제 해결
    • prop이 객체, 배열 또는 함수일 때 컴포넌트가 리렌더링됩니다

참조

memo(Component, arePropsEqual?)

memo로 컴포넌트를 감싸 메모화된 버전의 컴포넌트를 얻을 수 있습니다. 메모화된 버전의 컴포넌트는 일반적으로 props가 변경되지 않는 한 부모 컴포넌트가 리렌더링할 때 같이 리렌더링하지 않습니다. 그러나 메모화는 성능을 최적화하지만 보장되는 것은 아니어서, React는 여전히 리렌더링 할 수도 있습니다.
아래에서 더 많은 예시를 확인하세요.

매개변수

  • Component: 메모화 하려는 컴포넌트입니다. memo는 이 컴포넌트를 수정하지 않고 새롭게 메모화된 컴포넌트를 반환합니다. 함수와 forwardRef 컴포넌트를 포함한 모든 유효한 React 컴포넌트가 허용됩니다.
  • optional arePropsEqual: 컴포넌트의 이전 prop 및 새로운 prop의 두 인자를 받는 함수입니다. 새로운 prop이 이전 prop과 같으면 true를 반환해야 합니다. 다시 말해, 새로운 prop으로도 컴포넌트가 동일한 출력을 만들고 동일한 방식으로 작동하는 경우입니다. 그렇지 않으면 false를 반환해야 합니다. 일반적으로 이 함수를 지정하지 않습니다. React는 기본적으로 Object.is로 각 prop을 비교합니다.

반환값

memo는 새로운 React 컴포넌트를 반환합니다. memo에 제공한 원본 컴포넌트와 동일하게 동작하지만, 부모가 리렌더링 하더라도 prop이 변경되지 않는 한 React는 이를 리렌더링하지 않습니다.

사용법

prop이 변경되지 않았을 때 리렌더링 건너뛰기

React는 일반적으로 부모가 리렌더링될 때마다 컴포넌트를 리렌더링 합니다. memo를 사용하면 부모 컴포넌트가 리렌더링될 때 새로운 props가 이전 props와 동일하면 리렌더링하지 않는 컴포넌트를 만들 수 있습니다. 이러한 컴포넌트를 메모화되었다고 합니다.
컴포넌트를 메모화하려면 memo로 감싸고, 반환된 값을 원래 컴포넌트의 자리에 사용하십시오.
React 컴포넌트는 항상 순수한 렌더링 로직을 가져야합니다. 이는 props, state 및 context가 변경되지 않으면 항상 동일한 출력을 반환해야 함을 의미합니다. memo를 사용하면 React에게 컴포넌트가 이같은 요구 사항을 준수한다고 알리므로, prop이 변경되지 않는 한 React는 리렌더링할 필요가 없습니다. 그러나 memo를 적용하더라도 컴포넌트의 state가 변경되거나 사용중인 context가 변경되면 리렌더링 합니다.
아래 예제에서 Greeting 컴포넌트는 name이 변경될 때마다 리렌더링 됩니다. name이 props 중 하나이기 때문입니다. 하지만 address는 Greeting의 prop이 아니기 때문에 address가 변경될 때는 리렌더링되지 않습니다.

Note

memo 없이 코드가 작동하지 않으면 먼저 근본적인 문제를 찾아 해결하세요. 이후에 memo를 추가하여 성능을 개선할 수 있습니다.
 
DEEP DIVE | 심층 탐구

모든 곳에 memo를 추가해야할까요?

이 사이트와 같이 대부분의 인터랙션이 투박한 앱의 경우(페이지 또는 전체 섹션 교체 등) 일반적으로 메모화는 불필요합니다. 반면 앱이 그림 편집기이고 (도형 이동과 같이) 대부분의 인터랙션이 세분화되어 있다면, 메모화가 매우 유용할 수 있습니다.
memo로 최적화하는 것은 컴포넌트가 정확히 동일한 props로 자주 리렌더링되고, 리렌더링 로직이 비용이 많이 들 때만 가치가 있습니다. 컴포넌트가 리렌더링되어도 인지할 수 있을 만큼의 지연이 없다면 memo가 필요하지 않습니다. memo는 컴포넌트에 전달되는 prop이 항상 다른 경우 (객체 또는 렌더링 중 정의된 일반 함수와 같은 경우) 완전히 무용지물입니다. 따라서 memo와 함께 useMemo 및 useCallback을 종종 필요로 합니다.
그 외의 경우에는 컴포넌트를 memo로 감싸는 것에 이점이 없습니다. 그렇다고 해서 크게 해가 되는 것도 아니기 때문에 일부 팀에서는 개별 사례에 대해 생각하지 않고 가능한 한 많이 메모하는 방식을 선택하기도 합니다. 이 접근 방식의 단점은 코드 가독성이 떨어진다는 것입니다. 또한 모든 메모화가 효과적이지는 않습니다: “항상 새로운” 값이 단 하나만 있어도 컴포넌트 전체의 메모화를 깨뜨리기엔 충분합니다.
실제로 몇 가지 원칙을 따르면 많은 메모화가 불필요해질 수 있습니다:
  1. 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 JSX를 자식으로 받아들이도록 하세요. 이렇게 하면 wrapper 컴포넌트가 자체 state를 업데이트할 때 React는 그 자식 컴포넌트가 다시 렌더링할 필요가 없다는 것을 알 수 있습니다.
  1. 로컬 state를 선호하고 필요 이상으로 state를 끌어올리지 마세요. 예를 들어, 최상위 트리나 전역 state 라이브러리에 폼이나 아이템이 호버되었는지와 같은 일시적 state를 두지 마세요.
  1. 렌더링 로직을 순수하게 유지하세요. 컴포넌트를 다시 렌더링했을 때 문제가 발생하거나 눈에 띄는 시각적 아티팩트가 생성된다면 컴포넌트에 버그가 있는 것입니다! 메모화하는 대신 버그를 수정하세요.
  1. state를 업데이트하는 불필요한 Effect를 피하세요. React 앱의 대부분의 성능 문제는 컴포넌트를 반복해서 렌더링하게 만드는 Effect에서 발생하는 업데이트 체인으로 인해 발생합니다.
  1. Effect에서 불필요한 의존성을 제거하세요. 예를 들어, 메모화 대신 일부 오브젝트나 함수를 Effect 내부나 컴포넌트 외부로 이동하는 것이 더 간단할 때가 많습니다.
특정 인터렉션이 여전히 느리게 느껴진다면 React 개발자 도구 profiler를 사용해 어떤 컴포넌트가 메모화를 통해 가장 큰 이점을 얻을 수 있는지 확인하고 필요한 경우 메모화 하세요. 이러한 원칙은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있게 해주므로 어떤 경우든 이 원칙을 따르는 것이 좋습니다. 장기적으로는 이 문제를 완전히 해결하기 위해 세분화된 메모화를 자동으로 수행하는 방법을 연구하고 있습니다.
 

state를 사용하여 메모화된 컴포넌트 업데이트

컴포넌트가 메모화된 경우에도 자체 state가 변경되면 리렌더링됩니다. 메모화는 부모에서 컴포넌트로 전달되는 props에만 연관이 있습니다.
state 변수를 현재 값으로 설정하면 React는 memo 없이도 컴포넌트 리렌더링을 건너뜁니다. 컴포넌트가 한 번 더 호출될 수도 있지만, 결과는 무시됩니다.

context를 사용하여 메모화된 컴포넌트 업데이트하기

컴포넌트가 메모화 되었더라도 사용 중인 context가 변경될 때 이 컴포넌트는 리렌더링됩니다. 메모화는 부모로부터 전달되는 props에만 연관이 있습니다.
일부 context의 일부가 변경될 때만 컴포넌트를 리렌더링 하려면, 컴포넌트를 두 개로 분할하세요. 외부 컴포넌트의 context에서 필요한 내용을 읽고, 메모화된 자식에게 prop으로 전달하세요.

props 변경 최소화하기

memo를 사용할 때 어떤 prop든 이전의 prop과 얕은 비교 결과 같지 않을 때마다 컴포넌트가 다시 작동합니다. 즉, React는 Object.is 비교를 사용하여 컴포넌트의 모든 prop을 이전 값과 비교합니다. Object.is (3, 3)은 true이지만 Object.is({}, {})는 false입니다.
memo를 최대한 활용하려면, props가 변경되는 시간을 최소화해야 합니다. 예를 들어, prop이 객체인 경우, useMemo를 사용하여 부모 컴포넌트가 해당 객체를 매번 다시 만드는 것을 방지하세요:
props 변경을 최소화하는 더 나은 방법은 props가 해당 요소에 필요한 최소한의 정보만을 받고 있는지 확인하는 것입니다. 예를 들어, 전체 객체 대신 개별 값을 사용할 수 있습니다:
때론 개별 값에 대해서도 더욱 가끔 바뀌는 값으로 투영할 수 있습니다. 예를 들어, 다음 컴포넌트는 값 자체가 아닌 값의 존재를 나타내는 불리언을 받습니다:
메모화된 컴포넌트에 함수를 전달해야 하는 경우, 아예 변경되지 않도록 컴포넌트 외부에서 선언하거나, useCallback을 사용하여 정의를 캐시하십시오.

사용자 정의 비교 함수 지정하기

드물게 메모화된 컴포넌트의 props 변경을 최소화하는 것이 불가능할 수 있습니다. 이 경우 React가 이전 props와 새 props를 비교할 때 얕은 동등성 대신 사용하도록 커스텀 비교 함수를 제공할 수 있습니다. 이 함수는 memo의 두 번째 인자로 전달됩니다. 새로운 props가 이전 props와 동일한 출력을 생성하는 경우에만 true를 반환해야 합니다. 그렇지 않으면 false를 반환해야 합니다.
이 경우 브라우저 개발자 도구의 성능 패널을 사용하여 비교 기능이 컴포넌트를 다시 렌더링하는 것보다 실제로 더 빠른지 확인하십시오. 어쩌면 놀랄 수도 있습니다.
성능 측정을 할 때 React가 상용 환경에서 실행되고 있는지 확인하십시오.

Pitfall | 함정

커스텀 arePropsEqual 구현을 제공하는 경우 함수를 포함하여 모든 props를 비교해야 합니다. 함수는 종종 부모 컴포넌트의 prop과 state를 클로저로 다룹니다. oldProps.onClick !== newProps.onClick일 때 true를 반환하면 컴포넌트가 onClick 핸들러 내에서 이전 렌더링의 prop과 state를 계속 “인식”하여 매우 혼란스러운 버그가 발생합니다.
작업 중인 데이터 구조가 알려진 제한된 깊이를 가지고 있다고 100% 확신하지 않는 한 arePropsEqual 내에서 깊은 비교를 수행하지 마십시오. 깊은 비교는 엄청나게 느려질 수 있으며 나중에 누군가가 데이터 구조를 변경하면 몇 초 동안 앱이 정지될 수 있습니다.

문제 해결

prop이 객체, 배열 또는 함수일 때 컴포넌트가 리렌더링됩니다

React는 이전 prop과 새 prop을 얕은 비교로 동등성을 파악합니다: 즉, 각각의 새 prop이 이전 prop과 참조가 동일한지 여부를 고려합니다. 부모가 다시 렌더링될 때마다 새 객체나 배열을 생성하면 설령 개별 요소들이 모두 동일하더라도 여전히 React는 변경된 것으로 간주합니다. 마찬가지로 부모 컴포넌트를 렌더링할 때 새 함수를 만들면 React는 함수의 정의가 동일하더라도 변경된 것으로 간주합니다. 이를 방지하려면 부모 컴포넌트에서 prop을 단순화하거나 메모화 하십시오.
import { memo } from 'react'; const SomeComponent = memo(function SomeComponent(props) { // ... });
const Greeting = memo(function Greeting({ name }) { return <h1>Hello, {name}!</h1>; }); export default 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>; });
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> </> ); }
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> ); });
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 }) { // ... });
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 }) { // ... });
function GroupsLanding({ person }) { const hasGroups = person.groups !== null; return <CallToAction hasGroups={hasGroups} />; } const CallToAction = memo(function CallToAction({ hasGroups }) { // ... });
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; }) ); }