[React] memo

Simon·2024년 1월 14일
post-thumbnail

memo란?

memo를 사용하면 props가 변경되지 않은 경우 컴포넌트가 리렌더링하는 것을 건너뛸(skip) 수 있습니다.

용법(usage)

  • props가 변경되지 않은 경우 리렌더링 스킵
  • 상태를 사용하여 메모된 컴포넌트 업데이트
  • 컨텍스트를 사용하여 메모된 컴포넌트 업데이트
  • props 변경 최소화
  • 사용자 정의 비교 함수 지정

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
memo로 컴포넌트를 감싸서 해당 컴포넌트의 메모화된 버전을 가져옵니다. 이렇게 감싸진 컴포넌트는 props가 변경되지 않는 한 상위 컴포넌트가 다시 렌더링 될 때 다시 렌더링 되지 않습니다. 그러나 리액트는 여전히 이를 다시 렌더링 할 수 있습니다. memo는 성능 최적화이지 보장해 주는 것이 아닙니다.

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
  // ...
});

매개변수

  • Component: 메모하고 싶은 컴포넌트입니다. 메모는 컴포넌트를 수정하진 않지만 대신 메모된 새 컴포넌트를 반환합니다. 함수 및 forwardRef 컴포넌트를 포함한 모든 유효한 컴포넌트가 허용됩니다.

  • (선택적) arePropsEqual: 두 개의 인수(컴포넌트의 이전 props와 새 props)를 받는 함수입니다. 이전 props과 새 props가 동일하면 true를 반환해야 합니다. 즉, 컴포넌트가 동일한 출력을 렌더링 합니다. 그렇지 않으면 false를 반환해야 합니다. 일반적으로 이 기능은 지정하지 않습니다. 기본적으로 React는 각 prop을 Object.is와 비교합니다.

반환값

memo는 새로운 React 컴포넌트를 반환합니다. props가 변경되지 않는 한 부모 컴포넌트가 다시 렌더링 될 때 React가 항상 다시 렌더링 하지 않는다는 점을 제외하면 memo로 감싼 컴포넌트와 동일하게 동작합니다.

props가 변경되지 않은 경우 리렌더링 스킵

React는 일반적으로 상위 컴포넌트가 다시 렌더링 될 때마다 해당 컴포넌트를 다시 렌더링합니다. 메모를 사용하면 새 prop이 이전 prop과 동일한 한 부모가 다시 렌더링 될 때 React가 다시 렌더링 하지 않는 컴포넌트를 만들 수 있습니다. 이러한 컴포넌트를 메모되었다고 합니다.

컴포넌트를 memo 하려면 구성요소를 memo로 감싸고 원래 컴포넌트 대신 반환되는 값을 사용하면 됩니다.

const Greeting = memo(function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
});

export default Greeting;

React 컴포넌트에는 항상 순수한 렌더링 논리가 있어야 합니다. 이는 props, state 및 context가 변경되지 않은 경우 동일한 출력을 반환해야 함을 의미합니다. 메모를 사용하면 React에 컴포넌트가 이 요구 사항을 준수한다는 것을 알릴 수 있습니다. 따라서 React는 props가 변경되지 않는 한 다시 렌더링 할 필요가 없습니다. memo를 사용하더라도 자체 상태가 변경되거나 사용 중인 컨텍스트가 변경되면 컴포넌트가 다시 렌더링 됩니다.

이 예에서 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>;
});

상태를 사용하여 메모된 컴포넌트 업데이트

컴포넌트가 메모되어도(여기서는 Greeting 컴포넌트) 자체 상태가 변경되면 여전히 리렌더링이 발생합니다. memo는 상위 컴포넌트에서 전달되는 props와만 관련이 있습니다. 따라서 name을 변경했을 때 리렌더링이 발생하고 email 변경 시 발생하지 않습니다. 추가로 Greeting 컴포넌트에 상태변수 greeting을 props로 전달받은 GreetingSelector 컴포넌트에서 값을 변경해도 리렌더링이 발생합니다.

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>
    </>
  );
}

컨텍스트를 사용하여 메모된 컴포넌트 업데이트

컴포넌트가 메모된 경우에도 사용 중인 컨텍스트가 변경되면 여전히 다시 렌더링 됩니다. 위에서 말했듯이 상위 컴포넌트에서 전달되는 props와 관련이 있습니다. name이 변경되지 않았음에도 컨텍스트 값으로 제공된 theme 상태 값이 변경되어서 리렌더링이 발생했습니다.

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>
  );
});


컨텍스트의 특정 값이 변경될 때만 컴포넌트를 렌더링 하고 싶으면 컴포넌트를 분할하여 외부 컴포넌트의 컨텍스트에서 필요한 값을 memo된 하위 컴포넌트에 props로 전달합니다.

props 변경 최소화

memo를 사용하면 props가 이전과 동일하지 않을 때마다 컴포넌트가 다시 렌더링 됩니다. 이것은 React가 Object.is 비교를 사용하여 컴포넌트의 모든 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 }) {
  // ...
});

memo된 컴포넌트에 함수를 전달해야 하는 경우에는 컴포넌트 외부에 선언하여 변경되지 않도록 하거나 useCallback을 사용하여 전달해야 합니다.

사용자 정의 비교 함수 지정

드문 경우지만 메모된 구성 요소의 props 변경을 최소화하는 것이 불가능할 수 있습니다. 이 경우 React가 얕은 비교를 사용하는 대신 이전 prop과 새 prop을 비교하는 데 사용할 사용자 정의 비교 함수를 제공할 수 있습니다. 이 함수는 memo의 두 번째 인수로 전달됩니다. 새 prop이 이전 prop과 동일한 출력을 생성하는 경우에만 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;
    })
  );
}

문제해결

props가 객체, 배열, 함수인 경우 컴포넌트가 다시 렌더링됩니다.
React는 이전 prop과 새 prop을 얕은 동일성으로 비교합니다. 즉, 각각의 새 prop이 이전 prop과 참조가 동일한지 여부를 고려합니다. 상위 컴포넌트가 다시 렌더링될 때마다 새 객체나 배열을 만드는 경우 개별 요소가 각각 동일하더라도 React는 여전히 props가 변경된 것으로 간주합니다. 마찬가지로, 상위 컴포넌트를 렌더링할 때 새 함수를 생성하면 React는 해당 함수의 정의가 동일하더라도 해당 함수가 변경된 것으로 간주합니다.

profile
포기란 없습니다.

0개의 댓글