React

[ React ] React Hooks - useMemo / Memoization

yebeen 2024. 8. 21. 11:43

 

 

useMemo란?

useMemo는 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook

 

 

예시코드
calculateValue

- 캐싱하려는 값을 계산하는 함수

- 순수해야한다 → 인자를 받지않고 모든 타입의 값을 반환할 수 있어야한다.

- 만약 다음 렌더링에서 dependencies가 바뀌지않았으면 동일한 값 , 바뀌었으면 calculateValue를 호출

const cachedValue = useMemo(calculateValue, dependencies);

 

  

 

예시코드

해당 코드는 TodoList가 상태를 업데이트하거나 부모로부터 오는 새로운 props를 받으면 filterTodos가 실행된다.

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

 

보통 계산이 빠르기때문에 문제가 되지않지만 

큰 배열을 필터링 혹은 변환하는 비용이 큰 연산을 수행하는 경우 문제가 생기기에

데이터가 변하지않았다면 아래와 같이 계산을 생략하는 것이 좋다.

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

 

 

사용하는 경우

1. useMemo에 입력하는 계산이 눈에 띌 정도로 느리는데 종속성이 거의 변경되지않는 경우

 

2. memo로 감싸진 컴포넌트에 props로 전달할 경우 값이 변경되지않았다면 렌더링을 건너뛰고 싶은 경우

단순히 자식 컴포넌트를 재렌더링하기싫으면 컴포넌트에 memo를 걸고

import { memo } from 'react';

const List = memo(function List({ items }) {
  // ...
});

 

 

부모로부터 props를 받는데 해당 변수가 함수의 결과값을 받는 경우엔 해당 함수의에 useMemo를 걸어야한다.

export default function TodoList({ todos, tab, theme }) {
  // 테마가 변경될 때 마다 다른 배열이 표시됩니다.
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      {/* ... List의 props는 동일하지 않으며 매번 다시 렌더링 됩니다. */}
      <List items={visibleTodos} />
    </div>
  );
}
export default function TodoList({ todos, tab, theme }) {
  // 재렌더링 사이에 계산을 캐싱하도록 React에 지시합니다...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // ...따라서 해당 종속성이 변경되지 않는 한...
  );
  return (
    <div className={theme}>
      {/* ...List에 동일한 props가 전달되어 재렌더링을 생략할 수 있습니다. */}
      <List items={visibleTodos} />
    </div>
  );
}

 

 

이를 제외한 계산을 useMemo로 감싸는 것에 대한 이득은 없다.

그러나 감싼다고해서 큰 문제가 되지않아 일부 개발에선 가능한 많이 메모하는 방식을 택하기도 한다.

단 이럴 경우 가독성이 떨어지는 것은 감안해야한다.

 

가독성을 높이기 위해선 useCallback을 사용하는 것도 하나의 방법이다.

useMemo code  () =>{ return()=>{} }

export default function Page({ productId, referrer }) {
  const handleSubmit = useMemo(() => {
    return (orderDetails) => {
      post('/product/' + productId + '/buy', {
        referrer,
        orderDetails
      });
    };
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

 

useCallback code () => {}

export default function Page({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

 

 

3. 전달할 값을 나중에 일부 Hook의 종속성으로 이용하는 경우

아래의 코드처럼 작성하면 메모이제이션의 목적을 무색하게 한다.

컴포넌트가 다시 렌더링되면 컴포넌트 본문 내부의 모든 코드가 다시 실행되기때문이다.

searchOptions가 렌더링시 다른 참조값을 반환하기에 일어나는 문제

function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // 🚩 주의: 컴포넌트 본문에서 생성된 객체에 대한 종속성

 

 

 

이를 해결하기위해 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이 변경될 때만 변경
  // ...

 

 

더 좋은 방법은 searchOptions를 useMemo 계산 함수 내부에 선언하는 것이다.

function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]); // ✅ allItems이나 text가 변경될 때만 변경
  // ...

 

 

 

 

주의사항

1. useMemo는 Hook이기에 컴포넌트 최상위 레벨 또는 자체 Hook에서만 호출할 수 있다.

2. 조건문이나 반복문 내부에 호출 불가능하다.

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 반복문에서는 useMemo를 호출할 수 없습니다.
        const data = useMemo(() => calculateReport(item), [item]);
        return (
          <figure key={item.id}>
            <Chart data={data} />
          </figure>
        );
      })}
    </article>
  );
}

 

 

해결책

각 항목에 대한 컴포넌트를 추출하고 개별 항목에 대한 데이터 메모하기

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ 최상위 수준에서 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>
  );
});

 

 

 

3. ()=>{} 화살표 함수는 객체를 반환하지않음을 기억해야한다.

  const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
  }, [text]);

 

({})과 같은 괄호를 추가해주면 해결가능 or 리턴문 꼭 쓰기

  const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
  }), [text]);

 

 

메모이제이션을 불필요하게 만드는 원칙

1. 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 JSX를 자식처럼 받아들이도록하기 [ jsx노드는 불변 ]

     → 이와 같은 방법을 사용하면 부모 컴포넌트를 업데이트하더라도 자식을 다시 렌더링할 필요가 없다.

      단 이는 편리한 방법은 아니다. 왜냐하면 조건부로 이 작업을 수행할 수 없기때문...

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

 

2. 지역상태를 선호하고 필요 이상으로 상태를 위로 올리지않기

      ex. 폼과 같이 일시적인 상태나 어떤 항목이 트리의 맨위에 위치하거나 전역 상태 라이브러리에 없도록한다.

 

3. 상태를 업데이트하는 불필요한 effect 피하기 + effect에 불필요한 종속성 제거하기

 

 

 

 

useMemo VS useCallback VS memo

useMemo

- 값이 계산에 의해 의존하며 계산이 자주 변경되지 않거나 복잡한 경우에 사용

- 불필요한 재계산을 피하는 것

 

useCallback

- 함수가 종속성 배열 내의 값이 변경되지 않는한 동일한 참조를 유지하도록 하기위해 사용

- 자식 컴포넌트에 함수를 props로 전달할 때  유용 [ 그렇지않으면 부모가 렌더링될 때 자식도 함께 렌더링 ] 

 

memo

memo는 컴포넌트의 불필요한 재렌더링을 방지하기 위해 사용

     → 해당 컴포넌트의 props가 변경되지 않으면 그 컴포넌트는 다시 렌더링되지 않는다.