개발새발 로그

React - useMemo에 대하여 본문

React

React - useMemo에 대하여

이즈흐 2023. 12. 11. 22:09

useMemo

useState와 useEffect가 익숙해졌다면

이제 컴포넌트 성능을 최적화하는 방법을 알아보자

 

컴포넌트 성능을 최적화를 사용되는 대표적인 Hook은 

useMemo와 useCallback이 있다.

 

이번에는 useMemo에 대해서 알아보자

 

useMemo의 Memo는 Memoization을 뜻한다.

 

Memoization이란

동일한 값을 리턴하는 함수를 반복적으로 호출해야한다면

맨 처음값을 계산할 때 해당 값을 메모리에 저장해서 

필요할 때마다  또 계산하지 않고 메모리에서 꺼내서 재사용을 하는 기법

 

여기서 먼저 알아야할 점이 있다.

  • 함수형 컴포넌트는 말그대로 함수라는 사실이다.
  • 그리고 함수형 컴포넌트가 렌더링이 된다는 것은 그 함수가 호출된다는 것이다.
  • 함수는 호출될 때마다 함수 내부에 정의되어있는 모든 변수들이 초기화된다

 

만약 어떤 함수 내에서 복잡하고 무거운 연산을 해서 값을 반환한다면 이는 굉장히 비효율적일 것이다.

왜냐하면 무의미한 계산을 반복해서 값을 반환하기 때문이다.

 

useMemo를 사용해서 이 상황을 해결할 수 있다.

 

처음에 함수를 호출해서 그 값을 메모리에 저장하고, 재호출되었을 때 메모리 값을 사용하는 것이다.

 

useMemo의 구조를 살펴보자

const cachedValue = useMemo(calculateValue, dependencies)
const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

 

useMemo의 첫 번째 인자는 콜백함수를 받는다.

우리가 메모이제이션할 값을 계산해서 리턴해주는 함수이다.

이 콜백함수가 리턴하는 값이 바로 useMemo가 리턴하는 값이다.

 

두번째 인자는 의존성 배열이다.

배열안 요소의 값이 업데이트될 때만 콜백함수를 다시 호출해서

메모이제이션 된 값을 업데이트해서 다시 메모이제이션을 해준다.

 

만약에 의존성 배열이 빈 배열을 가진다면

맨 처음 컴포넌트가 마운트 되었을 때만 값을 계산하고,

이후에는 항상 메모이제이션된 값을 꺼내와서 사용한다.

 

useMemo를 쉽게 설명하면 
아주 무거운 작업을하는 함수에 useMemo를 사용함으로써
다른 작업을 통해 State가 변경되어 재렌더링 되어도
무거운 작업을 하는 함수의 반환값은 변함이 없기때문에 재 호출을 하지 않게하는 것이다

 

 

 

useMemo는 꼭 필요할 때만!

이처럼 좋은 useMemo도 무분별하게 남용하면 오히려 성능이 떨어진다.

 

useMemo를 사용한다는 건 값을 재활용하기 위해서 따로 메모리를 소비해서 저장을 하는 것이다.

그렇기 때문에 불필요한 값들 까지 모두  메모제이션해버리면 오히려 성능이 악화될 수 있다.

 

useMemo는 필요할 때만 적절하게 사용하는게 중요하다.

 

 

useMemo를 객체에도 사용한다!

useEffect의 의존성 배열에 갹채타입을 넣는다면 어떻게 될까?

 

useEffect의 의존성 배열을 해당 값이 바뀔 때만 실행된다.

근데 그 안의 값이 객체 값이라면 

눈으로 보기에 값이 바뀌지 않았어도 렌더링잉 될 때 useEffect가 실행되게 된다.

 

왜그럴까?

 

이는 원시 타입과 객체타입의 차이이다.

 

원시값은 해당 값을 그대로 변수에 넣지만 

하지만 객체타입은 너무 크기 때문에 변수에 넣지 않고,

어떤 메모리상에 공간이 할당되어 그 메모리 안에 저장한 후에 그 주소값을 변수에 저장한다.

 

그러면 아무리 useEffect의 의존성 배열안의 값인 객체 값이 바뀌지 않았어도 

렌더링이 되면 객체를 저장했던 변수는 또 똑같은 객체를 할당 받는다.

 아무리 값이 똑같아도 사실은 참조하는 주소값이 다르기 때문이다.

 

이를 해결하기 위해서는 useMemo를 사용하면 된다.

 

 

하위 컴포넌트 재렌더링 skipping 방법!

 

만약 무거운 작업을 하는 함수의 반환 값을 하위 컴포넌트에 prop한다면 어떻게 할까?

export default function TodoList({ todos, tab, theme }) {
  //테마가 변경될 때마다 다른 배열이 표시됩니다...
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      {/* ... 따라서 List의 소품은 절대 동일하지 않으며 매번 다시 렌더링됩니다. */}
      <List items={visibleTodos} />
    </div>
  );
}

기본적으로 컴포넌트가 다시 렌더링될 때 React는 모든 자식들을 재귀적으로 다시 렌더링한다.

그렇기 때문에 많은 계산이 필요한 컴포넌트때문에 결과적으로  재렌더링을 느리게 한다.

 

이를 해결하기 위해서는 무거운 계산을 하는 컴포넌트를 memo로 감싸서 Prop이 마지막 렌더링과 동일할 때 재렌더링을 건너뛸 수 있다.

import { memo } from 'react';

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

 

이때 중요한 점은 visibleTodos라는 배열 데이터는 계속해서 바뀐다는 것이다.

 그러면 memo로 감싼다하더라도 최적화가 작동하지 않는다는 것이다.

 

이때는 useMemo가 유용하다.

export default function TodoList({ todos, tab, theme }) {
  // 리액트에게 리렌더링 사이에 계산을 캐시하도록 지시하세요...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] //...이러한 종속성이 변하지 않는 한...
  );
  return (
    <div className={theme}>
      {/* ...목록에 동일한 소품이 수신되며 재렌더링을 건너뛸 수 있습니다. */}
      <List items={visibleTodos} />
    </div>
  );
}

 

 

 

Hook의 종속성 메모하기

컴포넌트에서 직접 생성된 객체에 의존하는 계산이 있다고 가정하자

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

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

이렇게 객체에 의존하는 것은 메모화의 취지를 무색하게 한다.

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

searchOptions 객체를 생성하는 코드도 다시 렌더링할 때마다 실행된다.

 

searchOptions는 useMemo의 종속성이고, React는 종속성이 다르니까 매번 searchItem을 다시 계산한다.

 

이를 해결하기 위해서는 두가지 방법이 있다.

 

첫 번째로 객체 자체를 메모하는 것이다.

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

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // ✅ 모든 항목 또는 검색 옵션이 변경될 때만 변경됩니다.
  // ...

 

두 번째 방법은 객체선언을 useMemo 계산 함수 내부에서 수행하는 것이다.

function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]); // ✅ 모든 항목 또는 텍스트가 변경될 때만 변경
  // ...

 

 

함수를 메모제이션하고싶으면??

 

function() {...}가 다른 객체를 생성하는 것처럼 funtion(){}과 같은 함수선언과 ()=>{} 과 같은 표현식은 렌더링할 때마다 다른 함수를 생성한다.

새 함수를 만드는 것 자체는 문제가 되지 않는데 함수를 만약 memo되어있는 하위 컴포넌트에 prop해서 주는 경우에는 props가 변경되지 않았을 때 재 렌더링을 피하고 싶을 것이다.

함수가 새로 생성되면 props가 달라지게되고, 메모제이션의 취지가 무색해지기 때문입니다!

 

이를 위해서는 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에는 이를 위해 특별히 내장된 Hook이 있다.

중첩된 함수를 추가로 작성할 필요없이 함수를 useMemo에서 useCallback으로 감싸면 된다.

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

  return <Form onSubmit={handleSubmit} />;
}
728x90
반응형
LIST

'React' 카테고리의 다른 글

React - useCallback에 대하여  (0) 2023.12.13
React - 커스텀 훅을 만들면서  (0) 2023.12.12
React - useContext에 대하여  (1) 2023.12.11
React - useRef에 대하여  (0) 2023.12.11
React - useState에 대하여  (0) 2023.12.11