본문 바로가기
프로그래밍/React

[React] useMemo

by YuminK 2023. 11. 29.

useMemo

useMemo is a React Hook that lets you cache the result of a calculation between re-renders.

 

const cachedValue = useMemo(calculateValue, dependencies)

 

Parameters 

calculateValue: The function calculating the value that you want to cache. It should be pure, should take no arguments, and should return a value of any type. React will call your function during the initial render. On next renders, React will return the same value again if the dependencies have not changed since the last render. Otherwise, it will call calculateValue, return its result, and store it so it can be reused later.

 

dependencies: The list of all reactive values referenced inside of the calculateValue code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison.

 

초기에 calculateValue의 결과를 반환한다. 다음 렌더링에서 이미 저장된 값을 반환한다. (의존성이 변하지 않았다면) 혹은 calculateValue를 다시 호출하여 결과를 다시 반환한다. 

 

리액트는 일반적으로 리렌더링시 컴포넌트를 전부 다시 그린다. 그리고 이는 빠르게 처리되므로 문제가 되지 않는다. 그러나 필터링, 큰 배열에 대한 변환, 비싼 작업을 하고 있는 경우 동작을 skip하고 싶을 것이다. 이런 상황에서 useMemo를 사용하여 이전 렌더링 시기와 동일한 결과값을 받을 수 있다. 이런 종류의 캐싱을 메모이제이션이라 한다. 

 

How to tell if a calculation is expensive? 

 

일반적으로 수천개의 오브젝트를 생성하거나 루프를 도는 경우가 아니라면 연산이 딱히 비싸진 않다. 만약 확실하게 확인하고 싶은 경우 다음처럼 로그를 추가할 수 있다.

 

console.time('filter array');

const visibleTodos = filterTodos(todos, tab);

console.timeEnd('filter array');

 

만약 나오는 로그가 1ms 혹은 그 이상이라면 메모이제이션을 사용하는 게 나을 수 있다. useMemo를 사용한 이후에 시간을 체크해보면 알 수 있을 것이다. useMemo는 처음 렌더링에서 빠르지 않으며, 업데이트 시기에 불필요한 작업을 넘기는 것이다. 

 

Chrome에서 제공하는 CPU 쓰로틀링 기능같은 것을 사용하여 인위적 감속을 사용하는 것도 좋은 아이디어다. 

StrictMode에서 기본적으로 2번씩 호출이 되니, 프로덕션 환경에서 체크하는 것이 더 정확한 결과가 나올 것이다. 

 

Should you add useMemo everywhere? 

 

useMemo를 사용하는 것은 다음 상황에서 의미가 있다.

1. useMemo가 들어가는 계산이 유의미하게 느리며 의존성이 많이 변하지 않는 경우

2. memo로 감싸진 컴포넌트에 prop을 넘기고 값이 변하지 않은 경우 렌더링을 스킵하고 싶은 경우. 메모이제이션은 오직 의존성이 변경된 경우에만 다시 처리하도록 한다.

3. 다른 훅의 의존성으로 값이 사용되는 경우, 예를 들면 해당 값을 useEffect 혹은 useMemo의 의존성으로 사용되는 경우

 

다른 경우에 useMemo로 래핑하는 것은 이득이 없다. 그렇게 했을 때 딱히 안 좋은 점도 없다. 가능한 많이 useMemo를 사용하는 것은 코드를 읽기 어렵게 한다. 또한 그렇게 효율적이지도 않다. 매번 새로운 값은 전체 컴포넌트의 메모이제이션을 부시기에 충분하다.

 

메모이제이션이 없는 코드 역시 자주 정상적으로 동작한다. 대부분의 경우 충분히 빠르지만, 렌더링하는 오브젝트가 유의미하게 늘어나는 경우, 리렌더링시 오버헤드가 많이 발생하게 된다. (필요한 시기에 메모이제이션을 잘해라)

 

다음 상황에서 최적화를 하려면...

 

export default function TodoList({ todos, tab, theme }) {

  // ...

  return (

    <div className={theme}>

      <List items={visibleTodos} />

    </div>

  );

}

 

List 컴포넌트를 memo로 감싸고 items가 변경될 때만 처리되도록 할 수 있다.

 

import { memo } from 'react';

 

const List = memo(function List({ items }) {

  // ...

});

 

만약에 넘겨주는 props 자체가 매번 변경이 된다면 렌더링 스킵 처리가 일어나지 않는다. (주의)

 

export default function TodoList({ todos, tab, theme }) {

  // Every time the theme changes, this will be a different array...

  const visibleTodos = filterTodos(todos, tab);

  return (

    <div className={theme}>

      {/* ... so List's props will never be the same, and it will re-render every time */}

      <List items={visibleTodos} />

    </div>

  );

}

 

이런 부분에서 함수를 메모이제이션하여 사용하여 컴포넌트가 다시 그려지지 않도록 할 수 있다.

 

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 items={visibleTodos} />

    </div>

  );

}

 

useMemo를 사용하는 것은 명확한 이유가 있어야 한다. 예를 들면 memo로 감싸진 컴포넌트에게 props를 전달해야 하다거나.

 

다음과 같이 memo를 사용하지 않고 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 items={visibleTodos} />는 그저 { type: List, props: { items: visibleTodos } } 같은 오브젝트이다.

 

Creating this object is very cheap, but React doesn’t know whether its contents is the same as last time or not. This is why by default, React will re-render the List component.

 

객체를 생성하는 것은 매우 싸지만, 리액트는 내부에 있는 컨텐츠가 동일한지 알지 못한다. 이것이 리액트가 기본적으로 List 컴포넌트를 다시 그리는 이유다. 그러나 만약 리액트가 이전 렌더와 동일한 JSX라고 인지하면 다시 렌더링하려고 시도하지 않을 것이다. JSX 노드는 immutable하니까. JSX 노드는 시간에 따라 변경되지 않기에, 리액트는 렌더링을 스킵하는 게 안전함을 인지하고 있다. 그러나 이것이 동작하게 하기 위해서 노드가 실제로 동일한 오브젝트여야 한다. (단순히 똑같아 보이는 것이 아니라) 이것이 이 예제에서 useMemo가 사용된 이유다. 수동으로 JSX 노드를 useMemo로 감싸는 것은 편리하지 않다. 예를 들어 조건부로 처리할 수 없다. 이것이 memo로 JSX를 감싸는 대신 memo로 처리하는 이유이다.

 

flutter 같은 경우에는 특정 부분이 Provider에 감싸져 있으면, 해당 부분만 builder 패턴을 이용하여 다시 그린다. 내부 함수의 호출이 지정한 범위에서만 일어난다. 리액트의 경우에는 기본적으로 다시 그리는데, 캐싱된 데이터를 이용하는지(메모이제이션)에 차이가 있다. 메모이제이션 여부에 따라 다시 렌더링을 할지 말지, 결정하여 구분하는 것이다. 

 

Flutter는 다시 그려야 하는 부분의 범위를 지정하는 느낌이 강하다. React의 경우 메모이제이션의 처리를 컴포넌트를 기준으로 하는 경향이 크다. Flutter 같은 경우에는 범위를 지정하여 Provider로 감싸서 사용하고, 리액트는 컴포넌트로 빼서 처리한다. 이 부분에서 느낌이 꽤 다른 것 같다.

 

리액트는 ... Object 생성하는 부분에서 연산을 많이 먹지 않는다고 한다. 다만 내부에 들어가는 연산의 캐싱은 신경쓴다. Flutter는 "이 부분만 다시 그려야지" 범위 지정의 느낌이 강하다.

 

리액트에서 말하는 상태의 변경이란, immutable한 객체를 다시 만드는 것을 의미하는데 Flutter는 그냥 클래스가 상태를 들고 있는다.

객체를 복사해서 수정하여 반환하거나, 새로 만들거나 vs 클래스가 상태 변수를 들고 있음의 차이

Functional Programming vs Object Oriented Programming 패러다임 차이 

 

매번 오브젝트를 생성하고 의존성으로 넘기는 경우, 메모이제이션 처리가 동작하지 않는다. (주의)

 

function Dropdown({ allItems, text }) {

  const searchOptions = { matchMode: 'whole-word', text };

 

  const visibleItems = useMemo(() => {

    return searchItems(allItems, searchOptions);

  }, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body

  // ...

 

이럴 때는 해당 값을 메모이제이션하고 이를 의존성으로 넘겨주는 식으로 처리할 수 있다.

 

function Dropdown({ allItems, text }) {

  const searchOptions = useMemo(() => {

    return { matchMode: 'whole-word', text };

  }, [text]); // ✅ Only changes when text changes

 

  const visibleItems = useMemo(() => {

    return searchItems(allItems, searchOptions);

  }, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes

  // ...

 

그러나 가장 나은 해결책은 그냥 내부에서 만드는 것이 가장 낫다.

 

function Dropdown({ allItems, text }) {

  const visibleItems = useMemo(() => {

    const searchOptions = { matchMode: 'whole-word', text };

    return searchItems(allItems, searchOptions);

  }, [allItems, text]); // ✅ Only changes when allItems or text changes

  // ...

 

추청하건데, Form은 memo를 사용하고 있을 것이고 함수를 props로 넘기고 싶을 것이다. 다만 이런 식으로 하는 경우 매 렌더링마다 함수가 생성되어 들어간다.

 

export default function ProductPage({ productId, referrer }) {

  function handleSubmit(orderDetails) {

    post('/product/' + productId + '/buy', {

      referrer,

      orderDetails

    });

  }

 

  return <Form onSubmit={handleSubmit} />;

}

 

따라서 다음처럼 함수를 메모이제이션하여 처리할 수 있다.

 

export default function Page({ productId, referrer }) {

  const handleSubmit = useMemo(() => {

    return (orderDetails) => {

      post('/product/' + productId + '/buy', {

        referrer,

        orderDetails

      });

    };

  }, [productId, referrer]);

 

  return <Form onSubmit={handleSubmit} />;

}

 

다만 추가적인 중첩 함수를 피하기 위해 useCallback을 사용하자. 

 

export default function Page({ productId, referrer }) {

  const handleSubmit = useCallback((orderDetails) => {

    post('/product/' + productId + '/buy', {

      referrer,

      orderDetails

    });

  }, [productId, referrer]);

 

  return <Form onSubmit={handleSubmit} />;

}

 

React에 따르면 useCallback은 useMemo를 이용하여 개발되어 있다고 한다. (따라서 위 예제는 동일함)

 

오브젝트를 메모이제이션할 때는 다음과 같이 사용할 수 없다. 

 

  // 🔴 You can't return an object from an arrow function with () => {

  const searchOptions = useMemo(() => {

    matchMode: 'whole-word',

    text: text

  }, [text]);

 

이건 동작하는데, 실수의 여지가 있다. 

 

  // This works, but is easy for someone to break again

  const searchOptions = useMemo(() => ({

    matchMode: 'whole-word',

    text: text

  }), [text]);

 

그냥 이렇게 써라. 

 

  // ✅ This works and is explicit

  const searchOptions = useMemo(() => {

    return {

      matchMode: 'whole-word',

      text: text

    };

  }, [text]);

 

의존성 리스트를 추가하지 않았다면, 매 렌더링마다 함수를 호출하여 결과를 반환한다. (주의)

 

function TodoList({ todos, tab }) {

  // 🔴 Recalculates every time: no dependency array

  const visibleTodos = useMemo(() => filterTodos(todos, tab));

  // ...

 

function TodoList({ todos, tab }) {

  // ✅ Does not recalculate unnecessarily

  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

  // ...

 

루프 내에서 useMemo를 사용할 수 없다.

 

function ReportList({ items }) {

  return (

    <article>

      {items.map(item => {

        // 🔴 You can't call useMemo in a loop like this:

        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:

  const data = useMemo(() => calculateReport(item), [item]);

  return (

    <figure>

      <Chart data={data} />

    </figure>

  );

}

 

아니면 컴포넌트 자체를 메모하여 사용할 수 있다.

 

function ReportList({ items }) {

  // ...

}

 

const Report = memo(function Report({ item }) {

  const data = calculateReport(item);

  return (

    <figure>

      <Chart data={data} />

    </figure>

  );

});

 

https://react.dev/reference/react/useMemo

'프로그래밍 > React' 카테고리의 다른 글

[React] forwardRef  (0) 2023.11.29
[React] memo  (1) 2023.11.29
[React] useCallback  (1) 2023.11.28
[React] useRef  (0) 2023.11.28
[React] useEffect  (0) 2023.11.28

댓글