Hooks - useMemo

흑우·2023년 8월 11일

useMemo(calculateValue, dependencies)

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

참조

선언하기

  • 컴포넌트 최상단에서 useMemo를 호출하여 리렌더링 사이의 계산 결과를 캐시합니다.
function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

매개변수

calculateValue

  • 캐시하려는 값을 계산하는 함수입니다. 이 함수는 순수함수여야 하며, 인자를 받지 않고, 반드시 어떤 타입이든 값을 반환해야 합니다.
  • React는 초기 렌더링 중에 함수를 호출합니다. 이후의 렌더링에서는 의존성이 이전 렌더링 이후 변경되지 않았다면 동일한 값을 반환합니다.
  • 그렇지 않으면 calculateValue를 호출하고 그 결과를 반환하며, 나중에 재사용할 수 있도록 저장합니다.

dependencies

  • calculateValue 코드 내에서 참조되는 모든 반응형 값들의 목록입니다. 반응형 값에는 props, state 및 컴포넌트 본문 내에서 직접 선언된 모든 변수와 함수가 포함됩니다.
  • React는 Object.is 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교합니다. (React는 값이 변했는지를 대부분 Object.is로 체크하네요.)

반환값

  • 초기 렌더링에서 useMemo는 인자 없이 calculateValue를 호출한 결과를 반환합니다.
  • 이후 렌더링에서는, 의존성이 변경되지 않은 경우에는 마지막 렌더링에서 저장된 값을 반환하고, 변경된 경우에는 calculateValue를 다시 호출하여 그 결과를 반환합니다. (의존성이 변경되면 새롭게 계산하고 아니면 원래 값을 반환 해주네요)

주의사항

  • useMemo는 훅이므로 컴포넌트의 최상위 레벨 또는 커스텀 훅에서만 호출할 수 있습니다. 반복문이나 조건문 안에서 호출할 수 없습니다.
    • 만약 필요하다면, 새로운 컴포넌트를 생성하고 해당 컴포넌트로 state를 이동시키세요. (이 부분은 지겹게도 많이 나오네요. 모든 hooks들의 공통점이라 제외할까 생각중입니다.)
  • Strict 모드에서는 React가 의도치 않은 불순물을 찾기 위해 계산 함수를 두 번 호출합니다. 이는 개발 전용 동작이며 상용 환경에서는 영향을 미치지 않습니다.
    • 계산 함수가 순수하다면(그래야 합니다) 이것은 컴포넌트의 로직에 영향을 미치지 않을 것입니다. 두 번의 호출 중 하나의 결과는 무시됩니다. (이부분 또한 지겹도록 많이 나오네요)
  • React는 특별한 이유가 있지 않는 한 캐시된 값을 유지하려고 합니다. 예를 들어, React는 개발 중에 컴포넌트 파일을 수정하면 캐시된 값을 폐기합니다.
  • 개발 환경 및 상용 환경 모두에서, 초기 마운트 중에 컴포넌트가 일시 중단(suspend)되면 React는 캐시를 폐기합니다. 미래에 React는 캐시를 폐기하는 것을 활용하는 더 많은 기능을 추가할 수 있습니다.
  • 예를 들어, 미래에 React가 가상화된 목록에 대한 기본 지원을 추가한다면, 가상화된 테이블 뷰포트에서 벗어난 항목에 대한 캐시를 폐기하는 것이 타당할 것입니다. useMemo를 성능 최적화를 위해서만 사용하는 경우에는 괜찮을 것입니다. 그렇지 않다면 state variable 또는 ref가 더 적절할 수 있습니다. (해당 부분이 useMemo의 주의사항 같습니다.)

😀 사용법

비용이 많이 드는 재계산 생략하기

  • 리렌더링간의 계산값을 캐시하려면 컴포넌트의 최상단에서 useMemo 호출로 해당 값을 감싸세요.
function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}
  • useMemo에는 두 가지를 전달해야 합니다.
    • 인자(argument)를 받지 않고(() =>), 원하는 값을 계산하여 반환하는 계산 함수.
    • 컴포넌트 내에서 계산에 사용되는 모든 값을 포함하는 의존성 목록.
  • 초기 렌더링 시에는, useMemo를 통해 얻는 값은 계산 함수를 호출한 결과값입니다.
  • 그 이후 모든 렌더링에서, 의존성이 변경되지 않았다면(Object.is), useMemo는 이전에 계산했던 값을 반환합니다. 그렇지 않다면, React는 계산을 다시 실행하고 새로운 값을 반환합니다.
  • 간단히 말해서, useMemo는 의존성이 변경되기 전까지 계산 결과를 캐시합니다.

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

  • 콘솔 로그를 추가하여 코드에 소요된 시간을 측정할 수 있습니다.
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
  • 전체적으로 기록된 시간이 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모해 두는 것이 좋습니다.
console.time('filter array');
const visibleTodos = useMemo(() => {
  return filterTodos(todos, tab);
                                  // todos 및 tab이 모두 변경되지 않으면 건너뜁니다
}, [todos, tab]);
console.timeEnd('filter array');
  • 그런 다음 실험 삼아 해당 계산을 useMemo로 감싸보고, 해당 상호작용에 대해 총 로깅 시간이 감소했는지 여부를 확인할 수 있습니다.
  • 컴퓨터가 사용자 컴퓨터보다 빠를 수 있으므로 인위적으로 속도를 늦춰 성능을 테스트하는 것이 좋습니다. 예를 들어, Chrome은 이를 위한 CPU 쓰로틀링 옵션을 제공합니다.
  • 또한 개발 중에 성능을 측정하는 것은 반드시 정확한 결과를 제공하지는 않는다는 점에 유의하세요.
  • (예를 들어, Strict Mode를 켜면 각 컴포넌트가 한 번이 아닌 두 번 렌더링되는 것을 볼 수 있습니다.) 가장 정확한 타이밍을 얻으려면 상용 앱을 빌드하고 사용자가 사용하는 것과 동일한 기기에서 테스트하세요.

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

  • useMemo를 통한 최적화는 몇 가지 경우에만 유용합니다.
    • useMemo에 넣는 계산이 눈에 띄게 느리고 의존성이 거의 변하지 않는 경우.
    • memo로 감싼 컴포넌트에 prop으로 전달하는 경우. 값이 변경되지 않은 경우 리렌더링을 건너뛰고 싶을 수 있습니다. 메모화하면 의존성이 동일하지 않은 경우에만 컴포넌트를 다시 렌더링할 수 있습니다.
    • 전달한 값은 나중에 어떤 훅의 의존성으로 사용될 것입니다. 예를 들어, 또다른 useMemo 또는 useEffect에서 이 값에 의존하고 있을 수 있습니다.
  • **실제로 몇 가지 원칙을 따르면 많은 메모화가 불필요해질 수 있습니다
    • 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 JSX를 자식으로 받아들이도록 하세요. 이렇게 하면 wrapper 컴포넌트가 자체 state를 업데이트할 때 React는 그 자식 컴포넌트가 다시 렌더링할 필요가 없다는 것을 알 수 있습니다. (이건 몰랐네요.. 꿀팁입니다.)
    • 로컬 state를 선호하고 필요 이상으로 state를 끌어올리지 마세요. 예를 들어, 최상위 트리나 전역 state 라이브러리에 폼이나 아이템이 호버되었는지와 같은 일시적 state를 두지 마세요.
    • 렌더링 로직을 순수하게 유지하세요. 컴포넌트를 다시 렌더링했을 때 문제가 발생하거나 눈에 띄는 시각적 아티팩트가 생성된다면 컴포넌트에 버그가 있는 것입니다! 메모화하는 대신 버그를 수정하세요.
    • state를 업데이트하는 불필요한 Effect를 피하세요. React 앱의 대부분의 성능 문제는 컴포넌트를 반복해서 렌더링하게 만드는 Effect에서 발생하는 업데이트 체인으로 인해 발생합니다. (이 부분이 많이 어려운 거 같습니다. 불필요한 Effect와 의존성을 없애는 작업을 저도 잘하고 싶네요 ㅠ)
    • Effect에서 불필요한 의존성을 제거하세요. 예를 들어, 메모화 대신 일부 오브젝트나 함수를 Effect 내부나 컴포넌트 외부로 이동하는 것이 더 간단할 때가 많습니다.
  • 특정 인터렉션이 여전히 느리게 느껴진다면 React 개발자 도구 profiler를 사용해 어떤 컴포넌트가 메모화를 통해 가장 큰 이점을 얻을 수 있는지 확인하고 필요한 경우 메모화 하세요. (성능 측정할 때 유용해보입니다.)

useMemo와 값을 직접 계산하는 것의 차이점

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
  • 해당 코드에서는 theme props가 변경될 때마다 TodoList가 재랜더링 되며, filterTods함수가 재실행 됩니다. 이럴 경우 데이터는 변함이 없기 때문에 useMemo로 감싸서 재계산을 건너뛸 수 있습니다.
 const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );

😀 컴포넌트의 리렌더링 건너뛰기

  • 어떤 경우에는 useMemo를 사용하여 자식 컴포넌트의 리렌더링 성능을 최적화할 수도 있습니다.
  • 이를 설명하기 위해, TodoList 컴포넌트가 visibleTodos를 자식 List 컴포넌트에 prop으로 전달한다고 가정해 보겠습니다.
export default function TodoList({ todos, tab, theme }) {
  // ...
  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}
  • theme prop을 전환하면 앱이 잠시 동안 멈추지만, JSX에서 를 제거하면 빠르게 동작하는 것을 확인했습니다. List 컴포넌트의 최적화를 시도해볼 가치가 있겠습니다.
  • 기본적으로 컴포넌트가 리렌더링되면 React는 모든 자식 컴포넌트를 재귀적으로 리렌더링합니다.
    • 이는 리렌더링에 많은 계산이 필요하지 않은 컴포넌트의 경우에는 괜찮습니다. 그러나 리렌더링이 느리다는 것을 확인했다면,
    • 이전 렌더링과 동일한 prop이 있는 경우 List가 리렌더링을 건너뛰도록 memo로 감싸줄 수 있습니다.
import { memo } from 'react';

const List = memo(function List({ items }) {
  // ...
});
  • 이 변경으로 인해 List는 모든 prop이 이전 렌더링과 같은 경우에는 리렌더링을 건너뛸 것입니다.
  • 이는 캐싱 계산이 중요해지는 부분입니다! useMemo를 사용하지 않고 visibleTodos를 계산했다고 상상해 보세요.
export default function TodoList({ todos, tab, theme }) {
  // theme가 변경될 때마다 매번 다른 배열이 됩니다...
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      {/* ... List의 prop은 절대로 같을 수 없으므로, 매번 리렌더링할 것입니다 */}
      <List items={visibleTodos} />
    </div>
  );
}
  • 위 예제에서는 filterTodos 함수가 항상 다른 배열을 생성합니다.
    • 이는 {} 객체 리터럴이 항상 새로운 객체를 생성하는 것과 비슷합니다.
    • 이는 일반적으로는 문제가 되지 않지만, List의 prop은 결코 같은 값을 가질 수 없고, 따라서 memo 최적화도 작동하지 않음을 의미합니다. 바로 이럴 때 useMemo가 유용합니다.
export default function TodoList({ todos, tab, theme }) {
  // 리렌더링 사이에 계산 결과를 캐싱하도록 합니다...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
                 // ...따라서 여기의 의존성이 변경되지 않는다면 ...
  );
  return (
    <div className={theme}>
      {/* ...List는 같은 props를 전달받게 되어 리렌더링을 건너뛸 수 있게 됩니다 */}
      <List items={visibleTodos} />
    </div>
  );
}
  • visibleTodos 계산을 useMemo로 감싸면, 리렌더링 사이에 동일한 값이 보장됩니다
    (useMemo와 memo()의 차이와 관계성을 보여주는 좋은 부분이네요. 객체는 참조형 데이터이기 때문에 {}이어도 항상 다른 값을 가르키죠.)

개별 JSX 노드 메모화

  • List를 memo로 감싸는 대신 JSX 노드 자체를 useMemo로 감쌀 수 있습니다.
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>
  );
}
  • 동작은 동일합니다. visibleTodos가 변경되지 않은 경우 List가 리렌더링되지 않습니다.
  • 같은 JSX 노드는 { type: List, props: { items: visibleTodos } }와 같은 객체입니다.
  • 이 객체를 생성하는 것은 비용이 적게 들지만, React는 그 내용이 지난번과 동일한지 아닌지 알지 못합니다. 그렇기 때문에 기본적으로 React는 List 컴포넌트를 다시 렌더링합니다.
  • 하지만 React는 이전 렌더링 때와 동일한 JSX를 발견하면 컴포넌트를 리렌더링하려고 시도하지 않습니다. JSX 노드는 불변이기 때문입니다.
  • JSX 노드 객체는 시간이 지나도 변경될 수 없으므로 React는 리렌더링을 건너뛰어도 안전하다는 것을 알고 있습니다. 다만 이것이 작동하려면 노드가 단순히 코드에서 동일하게 보이는 것이 아니라 실제로 동일한 객체여야 합니다. 이 예시에서 useMemo는 바로 이런 역할을 합니다.
  • JSX 노드를 수동으로 useMemo로 감싸는 것은 편리하지 않습니다. 예를 들어, 이 작업을 조건부로 수행할 수 없습니다. 일반적으로 JSX 노드를 useMemo로 감싸는 대신 memo로 컴포넌트를 감싸는 이유입니다.(이 부분이 중요하네요 ㅎㅎ )

리렌더링을 건너뛰는 것과 항상 리렌더링하는 것의 차이점

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>
      <List items={visibleTodos} />
    </div>
  );
}

const List = memo(function List({ items }) {
  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
});
  • 여기서 중요한 점은 useMemo와 memo가 없다면 filterTodos에서 동일한 값을 계산한다고 해도 List 컴포넌트는 리렌더링 된다는 점입니다. (왜냐? 리스트는 참조형 데이터이기 때문에!)

다른 훅의 의존성 메모화

  • 컴포넌트 본문에서 직접 생성한 객체에 의존하는 계산이 있다고 가정해 보겠습니다.
function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); 
                                 // 🚩 주의: 컴포넌트 내부에서 생성한 객체 의존성
  // ...
  • 이렇게 객체에 의존하는 것은 메모화의 취지를 무색하게 합니다. 컴포넌트가 다시 렌더링되면 컴포넌트 본문 내부의 모든 코드가 다시 실행됩니다. searchOptions 객체를 생성하는 코드 라인도 다시 렌더링할 때마다 실행됩니다.
  • searchOptions 는 useMemo 호출의 의존성이고 매번 다르기 때문에, React는 의존성이 지난번과 다르다는 것을 알고, 매번 searchItems 를 다시 계산합니다. (이러한 문제는 effect에서부터 꾸준히 나오네요.)
  • 이 문제를 해결하려면 searchOptions 객체를 의존성으로 전달하기 전에 객체 자체를 메모화할 수 있습니다.
function Dropdown({ allItems, text }) {
  const searchOptions = useMemo(() => {
    return { matchMode: 'whole-word', text };
  }, [text]); // ✅ text 변경시에만 변경됨
              

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); //✅ allItems 또는 searchOptions 변경시에만 변경됨
  // ...
                                 
  • 위의 예에서 text 가 변경되지 않았다면 searchOptions 객체도 변경되지 않습니다. 이보다 더 나은 수정 방법은 searchOptions 객체 선언을 useMemo 계산 함수 내부로 이동하는 것입니다.
function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]);  // ✅ allItems 또는 text 변경시에만 변경됨
                       
  // ...
  • 이제 계산은 (객체 처럼 “실수로” 다른 값이 될 수 없는 문자열) text에 직접 의존합니다.
  • useEffect 때 처럼 컴포넌트 밖에 선언 할 수 없습니다. 컴포넌트에서 받는 text라는 props를 받고 있기 때문인데요. 이렇게 useMemo로 감싸서 text가 변경될 때마다 option을 업데이트 할 수 있네요.

함수 메모화

  • 컴포넌트가 memo로 감싸져 있다고 가정해 봅시다. 여기에 함수를 prop으로 전달하려고 합니다.
export default function ProductPage({ productId, referrer }) {
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }

  return <Form onSubmit={handleSubmit} />;
}
  • {}가 다른 객체를 생성하는 것과 같이, function() {} 함수 선언 및 () => {} 표현식 등은 모두 리렌더링할 때마다 다른 함수를 생성합니다. 새 함수를 만드는 것 자체는 문제가 되지 않습니다.
  • 피해야 할 일이 아닙니다! 하지만 Form 컴포넌트가 메모화되어 있다면 props가 변경되지 않았을 때 리렌더링하는 것을 건너뛰고 싶을 것입니다. prop이 항상 달라지면 메모화의 취지가 무색해집니다.
  • useMemo로 함수를 메모화하려면 계산 함수가 다른 함수를 반환해야 합니다.
export default function Page({ productId, referrer }) {
  const handleSubmit = useMemo(() => {
    return (orderDetails) => {
      post('/product/' + productId + '/buy', {
        referrer,
        orderDetails
      });
    };
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}
  • 이는 투박해 보입니다! 함수를 메모화하는 것은 충분히 흔한 일이므로 React는 이를 위한 특별한 훅을 제공합니다. 중첩 함수를 추가로 작성할 필요가 없도록 함수를 useMemo 대신 useCallback으로 감싸세요. (왜 useCallback을 안 알려주나 했네요 ㅎㅎ)
export default function Page({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}
  • 위의 두 예제는 완전히 동일합니다. useCallback을 사용하면 내부에 중첩된 함수를 추가로 작성하지 않아도 된다는 장점이 있습니다. 그 외에는 다른 기능을 수행하지 않습니다.

문제 해결

1. 리렌더링할 때마다 계산이 두 번 실행됩니다.

  • 해당 문제는 정말 꾸준히 나오는 것 같습니다. 결론만 말씀드리자만 React는 Strict Mode에서 일부 함수를 두 번 호출하며, 두 번 호출했을 때 결과값이 변하면 안된다고 합니다.
  • 이것은 React에서 지향하는 방식이 순수 함수이기 때문인데요. 배열 같은 참조형 데이터를 수정할 때는 원본 배열을 직접 건드려서 변형 시키는 것보다는 새로운 배열로 교체하라고 하네요.
  const visibleTodos = useMemo(() => {
    // 🚩 Mistake: mutating a prop
    // 🚩 실수: prop 변이
    todos.push({ id: 'last', text: 'Go for a walk!' });
    const filtered = filterTodos(todos, tab);
    return filtered;
  }, [todos, tab]);

  const visibleTodos = useMemo(() => {
    const filtered = filterTodos(todos, tab);
    // ✅ Correct: mutating an object you created during the calculation
    // ✅ 올바름: 계산 중에 생성한 객체의 변이
    filtered.push({ id: 'last', text: 'Go for a walk!' });
    return filtered;
  }, [todos, tab]);

2. useMemo 호출이 객체를 반환하길 기대했으나 실제로는 undefined 객체를 반환합니다.

  • 해당 에러는 저도 가끔 실수 하는 에러인데요. 정말 단순합니다. 화살표 함수에서 객체를 리턴하고 싶을 때는 별도로 return을 명시해주거나 {}를 ()로 감싸야합니다.
  • 가독성을 위해서 return을 명시해주는 것을 권장하고 있네요.
 // 🔴 화살표 함수에서 () => { 만으로는 객체를 리턴할 수 없습니다.
  const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
  }, [text]);

// 이 코드는 작동하지만, 누군가에 의해 다시 꺠지기 쉽습니다
  const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
  }), [text]);

// ✅ 이 코드는 작동하며 명시적입니다
const searchOptions = useMemo(() => {
    return {
      matchMode: 'whole-word',
      text: text
    };
  }, [text]);

3. 컴포넌트가 렌더링될 때마다 useMemo의 계산이 다시 실행됩니다.

  • 두 번째 인자로 의존성 배열을 지정했는지 확인해야합니다! 의존성 배열이 없다면 매번 계산하니까요.
function TodoList({ todos, tab }) {
  // 🔴 매 번 재계산: 의존성 배열이 없음
  const visibleTodos = useMemo(() => filterTodos(todos, tab));
  // ...
  
function TodoList({ todos, tab }) {
  // ✅ 불필요하게 재계산하지 않음
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
  • 만약 의존성 배열을 지정했는데도 매번 계산을 진행한다면 의존성 중 하나 이상이 이전 렌더링과 다르기 때문입니다. 수동으로 콘솔에 의존성을 로깅하여 해결하세요!
 const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  console.log([todos, tab]);
  • 그런 다음 콘솔에서 서로 다른 리렌더링 시점의 배열을 마우스 오른쪽 버튼으로 클릭하고, 두 배열 모두에 대해 “전역 변수로 저장”을 선택할 수 있습니다.
  • 첫 번째 배열이 temp1로 저장되고 두 번째 배열이 temp2로 저장되었다고 가정하면, 브라우저 콘솔을 사용하여 두 배열의 각 의존성이 동일한지 확인할 수 있습니다.
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
                               // 각 배열의 첫번째 의존성이 동일한가?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
                               // 각 배열의 두번째 의존성이 동일한가?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
                               // ... 나머지 모든 의존성에 대해 반복 ...
  • 어떤 의존성이 메모화를 방해하는지를 찾았다면, 그 의존성을 제거할 방법을 찾거나, 함께 메모화하세요.

4. 루프에서 각 목록 항목에 대해 useMemo를 호출해야 하는데 허용되지 않는다고 합니다.

  • 당연합니다! hook은 컴포넌트의 최상단 위치에서만 선언할 수 있으니까요.
  • Chart 컴포넌트가 memo로 감싸져 있다고 가정해 봅시다. ReportList 컴포넌트가 리렌더링할 때 목록의 모든 차트를 리렌더링하는 것을 건너뛰고 싶을 수 있습니다. 하지만 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로 감쌀 수 있습니다. item prop이 변경되지 않으면 Report가 리렌더링을 건너뛰므로 Chart도 리렌더링을 건너뜁니다.
function ReportList({ items }) {
  // ...
}

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

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글