React

[React Hooks] useMemo 사용하기

제이콥(JACOB) 2019. 12. 10. 00:18

먼저 간단하게 `메모제이션`에 대해 알고 넘어가자. 

 

메모제이션(Memoization)

→ 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법.

→ 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 더 사용하는 경향이 있어도 성능 측면에서 큰 이점이 있어서 알고리즘 성능 최적화에 많이 사용된다.

컴퓨터프로그래밍용어로, 동일한 계산을 반복해야 할 경우 한 번 계산한 결과를 메모리에 저장해 두었다가 꺼내 씀으로써 중복 계산을 방지할 수 있게 하는 기법이다.동적 계획법의 핵심이 되는 기술로써 결국 메모리라는 공간 비용을 투입해 계산에 소요되는 시간 비용을 줄이는 방식이다. - 나무위키 -

→ 시간복잡도를 O(N)으로 줄인다.


useMemo

일반적으로 React의 함수형 컴포넌트는 다음과 같이 작성된다.

function Jacob({x, y}) {
	const z = lazyLogic(x, y);
	return <div>{z}</div>
}

이러한 컴포넌트는 앱이 랜더링 될때마다 호출된다.

(기본로직) 컴포넌트 함수가 호출 → 자바스크립트 로직 수행 → HTML 마크업된 UI 리턴

 

 하지만 랜더링이 한번 되고 끝나는게 아니라, 컴포넌트 자신의 상태 변경 또는 부모 컴포넌트의 상태 변경이 일어나 덩달아 함께 렌더링되야 하는 경우도 있다. 

 위 함수에서 lazyLogic은 세상에세 제일 느린 함수라고 가정해 보자. 아주 복잡하고 복잡한 로직이다. 그리고 값으로 받는 x, y 값이 항상 바뀌는게 아니라면 lazyLogic() 함수를 계속 실행해야할 필요가 있을까?

 

 바로 이러한 경우를 메모제이션이라는 개념을 통해 개선할 수 있으며, React hook에서는 v16.8 버전에서부터 기본적으로 내장돼있는 useMemo로 해결할 수 있다.

 useMemo()는 2개의 인자를 받는데, 첫번째는 결과 값을 생성해주는 팩토리 함수이고, 두번째는 결과값을 재활용할 때 기준이 되는 입력값 배열이다.

 

그럼 위의 함수를 개선해 보자.

import React, { useMemo } from 'react';

function Jacob({x, y}) {
	const z = useMemo(() => lazyLogic(x, y), [x, y]);
	return <div>{z}</div>
}

 x, y 값이 이전에 렌더링했을 때와 동일한 경우, 랜더링 때 구했던 결과값을 재활용 한다.

하지만, 이전과 값이 달라졌다면, ()=>lazyLogic(x, y)함수를 호출하여 z를 할당해 준다.


(2020-02-02 예제 추가) 기존 설명으로는 부족한 감이 있어, 실제로 useMemo()를 적용한 예제를 추가한다. 

더보기
import React, { useState, useMemo } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [wordIndex, setWordIndex] = useState(0);

  const words = ["H", "E", "L", "L", "O"];
  const word = words[wordIndex];

  const expensiveCompute = word => {
    console.log("expensiveComput() 호출!");
    let i = 0;
    while (i < 1000000000) i++;
    return word.length;
  };

  const wordsLength = useMemo(() => expensiveCompute(word), [word]);

  return (
    <div style={{ padding: "15px", border: "solid 1px" }}>
      <h2>매우 비싼 계산</h2>
      <p>
        "{word}"의 길이: {wordsLength}
      </p>
      <button
        onClick={() => {
          const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1;
          setWordIndex(next);
          setCount(count + 1);
        }}
      >
        버튼 클릭({count})
      </button>
    </div>
  );
}

export default App;

 

 위 예제는 단어의 길이를 구하는 엄청 복잡한 계산을 한다(사실 1만 반환되긴 하지만...).

아래 동작하는 모습을 확인해 보자. 

console.log에 오타가.. 에헤..

 계속 word의 값이 바뀌다가 ["H""E""L""L""O"]에서 인덱스 2와 3의 값이 같기 때문에

expensiveCompute() 함수를 다시 호출하여 계산을 하지 않고 캐싱된 값을 그대로 제공된다.


이 글을 마치며, 


소프트웨어 최적화에는 항상 그에 상응하는 대가가 따르기 마련이다.

성능 최적화를 함에 따라 얻는 이점이 더 많은지 꼭 따져보고 사용해야 한다. 

 

 예를 들면, useMemo hook함수를 남용하면, 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어렵고 유지보수성도 떨어진다. 또한 gc에서 제외되기 때문에 메모리를 더 쓰게된다. (← 추가설명: useMemo가 적용된 레퍼런스를 재활용하기 위해 가비지 컬렉션에서 제외된다.)

 

 잘 생각해보면 프론트에서 수 초이상 소요되는 로직을 구현하는 일 자체가 흔치 않다. 사용할 일이 많지 않는게 정상이다. 설사 있다고 해도 useEffect 함수 등을 이용해서 비동기로 처리하는 방안이 우선 고려된다. 

 

출처: 리액트 홈페이지^^;;

 

결론: 무분별한 useMemo를 사용하지 말고, 필요한 경우만 사용하자!

반응형