본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -useMemo

- useMemo는 리렌더링 사이의 계산 결과를 캐시할 수 있는 React 훅이다.

 

Parameters(매개변수)

- calculateValue : 캐시하려는 값을 계산하는 함수, 순수해야하며 인자를 받지 않고 반드시 어떤 타입이든 값을 반환해야 한다.

React는 초기 렌더링 중에 함수를 호출하고, 이후 렌더링에서는 의존성이 이전 렌더링 이후 변경되지 않았다면 동일한 값을 반환한다.

- dependencies : calculateValue 코드 내에서 참조되는 모든 반응형 값들의 목록.

React는 Object.is 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교한다.

 

Returns(반환값)

- 초기 렌더링에서 useMemo는 인자 없이 calculateValue를 호출한 결과를 반환한다.

- 이후 렌더링에서는 의존성이 변경되지 않으면 마지막 렌더링에서 저장된 값을 반환한다.

- 변경된 경우에는 calculateValue를 다시 호출하여 그 결과를 반환한다.


Skipping expensive recalculations(비용이 많이 드는 재계산 생략하기)

- useMemo가 언제 유용한지 예제로 살펴보자

function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab);
  // ...
}

- 위 예제에서 props가 변경되거나 state를 업데이트 했을 때마다 filterTodos 함수가 다시 실행된다.

- 하지만 만약 큰 배열을 필터링하거나, 변환하거나, 고비용의 계산을 수행할 때, 데이터가 변경되지 않았다면 다시 계산하는 과정을 건너뛰고 싶을 수 있고 위 예제에서는 todos, tab이 이전 렌더링 때와 동일하다면, useMemo를 사용해서 이전에 계산된 visibleTodos를 재사용할 수 있다.

- 이러한 종류의 캐싱을 메모화라고 한다.

 

 

* 비용이 많이 드는 계산인지는 어떻게 알 수 있나요?

- 콘솔 로그를 추가하여 코드에 소요된 시간을 측정할 수 있다.

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

- 위 예제의 측정 시간은 0.15ms와 같은 로그가 콘솔에 표시된다.

- 전체적으로 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모해 두는 것이 좋다.

- 이러한 속도 측정을 위해서는 인위적으로 속도를 늦춰 성능을 테스트하는 것이 좋으며, 가장 정확한 타이밍을 얻으려면 개발 모드가 아닌 상용 앱을 빌드하고 사용자가 사용하는 것과 동일한 기기에서 테스트 하는 것이 좋다.

 

* 모든 곳에 useMemo를 추가해야 하나요?

- 앱이 그림 편집기 처럼 대부분의 인터랙션이 도형 이동 처럼 세분화되어 있다면 메모화가 매우 유용할 수 있다.

- useMemo를 통한 최적화가 유용한 경우

1. useMemo에 넣는 계산이 눈에 띄게 느리고 의존성이 겨우 변하지 않는 경우

2. React.memo로 감싼 컴포넌트에 prop으로 전달하는 경우, 값이 변경되지 않는 경우 리렌더링을 건너뛰고 싶을 수 있다.

 

- 다음 원칙들을 따르면 useMemo가 불필요할 수도 있다.

1. 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 JSX를 자식으로 받아들이도록 하는 것이 좋다.(??)

function WrapperComponent({ children }) {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={incrementCount}>Increment</button>
      {children}
    </div>
  );
}

function App() {
  return (
    <WrapperComponent>
      <ChildComponent />
    </WrapperComponent>
  );
}

function ChildComponent() {
  console.log("ChildComponent rendered");
  return <p>Hello, I'm the child component.</p>;
}

2. 로컬 state를 선호하고 필요 이상으로 state를 끌어올리지 않는 것이 좋다.

3. 렌더링 로직을 순수하게 유지하자.

4. state를 업데이트하는 불필요한 Effect를 피하자.

5. Effect에서 불필요한 의존성을 제거하자 : 메모화 대신 일부 객체나 함수를 Effect 내부나 컴포넌트 외부로 이동하는 것이 간단할 때가 많다.

 

- 만약 특정 인터렉션이 여전히 느리게 느껴진다면 React 개발자 도구 profiler를 사용해서 어떤 컴포넌트가 메모화를 통해 가장 큰 이점을 얻을 수 있는지 확인하고 고민할 수 있다.


Memoizing individual JSX nodes(개별 JSX 노드 메모화)

export default function TodoList({ todos, tab, theme }) {
  // Tell React to cache your calculation between re-renders...
  // 리렌더링 사이에 계산 결과를 캐싱하도록 합니다...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // ...so as long as these dependencies don't change...
                 // ...따라서 여기의 의존성이 변경되지 않는다면 ...
  );
  return (
    <div className={theme}>
      {/* ...List will receive the same props and can skip re-rendering */}
      {/* ...List는 같은 props를 전달받게 되어 리렌더링을 건너뛸 수 있게 됩니다 */}
      <List items={visibleTodos} />
    </div>
  );
}

- 위 예제와 같이 visibleTodos를 useMemo로 감싸는 것 대신 <List /> JSX 노드 자체를 useMemo로 감쌀 수도 있다.

- React는 이전 렌더링 때와 동일한 JSX를 발견하면 컴포넌트를 리렌더링하려고 시도하지 않고 JSX 노드는 불변이기 때문에 React는 리렌더링을 건너뛴다. 즉, useMemo를 통해서 JSX가 동일한 객체임을 증명해주기 때문에, React는 리렌더링을 건너뛴다.

export default function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
  return (
    <div className={theme}>
      {children}
    </div>
  );
}

 


I need to call useMemo for each list item in a loop, but it's not allowed(루프에서 각 목록 항목에 대해 useMemo를 호출해야 하는데 허용되지 않는다고 합니다)

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 You can't call useMemo in a loop like this:
        // 🔴 루프 안에서는 useMemo를 호출할 수 없습니다:
        const data = useMemo(() => calculateReport(item), [item]);
        return (
          <figure key={item.id}>
            <Chart data={data} />
          </figure>
        );
      })}
    </article>
  );
}

- useMemo를 루프 안에서 호출할 수는 없다.

// 각 항목에 대한 컴포넌트를 추출하고 개별 항목에 대한 데이터를 메모화하는 방법
function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useMemo at the top level:
  // ✅ useMemo는 컴포넌트 최상단에서 호출하세요:
  const data = useMemo(() => calculateReport(item), [item]);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
}

// useMemo 대신 Report 컴포넌트 자체를 memo로 감싸는 방법
function ReportList({ items }) {
  // ...
}

const Report = memo(function Report({ item }) {
  const data = calculateReport(item);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
});

* 참고 : React 공식문서(https://react-ko.dev/learn)