Logo

React Hooks: useMemo 사용법

React Hooks 중 하나인 useMemo 함수를 왜/언제/어떻게 써야하는지 알아보겠습니다.

Memoization

useMemo 함수에 대해서 알아보기 전에 알고리즘 시간에 자주 나오는 메모이제이션(memoization) 개념에 대해서 잠깐 짚고 넘어가겠습니다. memoization이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. memoization을 절적히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화할 수 있습니다.

학술적으로 더 자세한 내용은 위키피디아를 참조바라겠습니다.

랜더링마다 호출되는 컴포넌트 함수

일반적으로 React의 함수형 컴포넌트는 다음과 같은 구조로 작성이 됩니다.

function MyComponent(props) {
  // 어떤 로직 (JavaScript)
  return; /* 어떤 화면 (JSX) */
}

이렇게 작성된 컴포넌트 함수는 React 앱에서 랜더링(rendering)이 일어날 때마다 호출이 됩니다. 컴포넌트 함수가 호출이 되면 그 안에 자바스크립트 로직들이 수행되고, 이를 기반으로 JSX로 마크업된 UI가 리턴되는 기본 구조를 가지고 있죠.

React에서 컴포넌트의 랜더링은 한 번 일어나고 끝이 아니라 수시로 계속 일어날 수 있습니다. 대표적인 예로 컴포넌트의 자신의 상태 변경(state update)이 일어날 수 있고, 아니면 부모 컴포넌트의 상태 변경이 일어나 덩달아 함께 랜더링되야 하는 경우도 있습니다. React에는 수동으로 다시 랜더링을 해주는 API도 있고, 사용자가 브라우저에서 새로고침을 할 때도 컴포넌트의 재 랜더링은 불가피 합니다.

함수형 컴포넌트의 실행이 느리다면?

아래 컴포넌트는 prop으로 넘어온 xy 값을 compute 함수에 인자로 넘겨서 z 값을 구한 후, 그 결과값을 div 엘리먼트로 감싸 출력해줍니다.

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

만약에, compute 함수가 내부적으로 매우 복잡한 연산을 수행하기 때문에 결과값을 리턴하는데 시간이 몇초 이상 오래 걸린다면 어떻게 될까요? 컴포넌트의 재 랜더링이 필요할 때 마다 이 함수가 호출이 되므로 사용자는 지속적으로 UI에서 지연이 발생하는 경험을 하게 될 것입니다.

함수형 컴포넌트에 memoization 적용

랜더링이 일어날 때 마다, compute 함수의 인자로 넘어오는 xy 값이 항상 바뀌는 게 아니라면 굳이 compute 함수를 계속 호출할 필요가 있을까요? 위와 같이 불편한 사용자 경험은 위에서 간단히 설명드린 memoization 기법을 적용하면 개선할 수 있습니다. 랜더링이 발생했을 때, 이전 랜더링과 현재 랜더링 간에 xy 값이 동일한 경우, 다시 함수를 호출을 하여 z 값을 구하는 대신, 기존에 메모리의 어딘가에 저장해두었던 z 값을 그대로 사용하는 것입니다.

이러한 상황에서 memoization 로직을 직접 구현할 수도 있겠지만, 대신에 간편하게 사용할 수 있는 것이 바로 React의 useMemo hook 함수입니다. useMemo 함수는 2개의 인자를 받는데, 첫번째는 결과값을 생성해주는 팩토리 함수이고, 두번째는 기존 결과값 재활용 여부의 기준이되는 입력값 배열입니다. 예를 들어, 다음과 같이 위에서 작성한 컴포넌트를 재작성하면,

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

xy 값이 이 전에 랜더링했을 때와 동일할 경우, 이 전 랜더링 때 저장해두었던 결과값을 재활용합니다. 하지만, xy 값이 이 전에 랜더링했을 때와 달라졌을 경우, () => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z에 할당해줍니다.

useMemo 임포트 방법

useMemo 함수는 React v16.8부터 기본적으로 내장되어 있는 Hooks 중 하나입니다. 따라서, 프로젝트에 react 패키지만 설치되어 있다면 named import를 통해 바로 임포트에서 사용할 수 있습니다.

import React, { useMemo } from "react";

[실습] useMemo 미적용

이제부터 간단한 실습 예제를 통해 실제로 useMemo hook 함수를 어떻게 활용할 수 있는지 알아보겠습니다. 단어 배열(words)을 상태(state)로 갖고, 추가된 순서대로 보여주는 목록(UnsortedWords)과 알파벳 순으로 정렬된 목록(SortedWords)으로 이뤄진 앱을 작성하려고 합니다.

먼저, 최상위 <App/> 컴포넌트를 작성하겠습니다. useState hook 함수를 이용해서 단어 배열(words)과 추가할 단어(word)를 컴포넌트의 상태(state)로 정의합니다.

useState hook 함수에 대한 자세한 설명은 관련 포스팅를 참고 바라겠습니다.

그리고 단어(word)를 단어 배열(words)에 추가하기 위한 <input/> 엘리먼트와 <button/> 엘리먼트도 작성합니다.

  • App.js
import React, { useState } from "react";
import UnsortedWords from "./UnsortedWords";
import SortedWords from "./SortedWords";

function App() {
  const [words, setWords] = useState([]);
  const [word, setWord] = useState("");

  const handleClick = () => {
    setWords([...words, word]);
    setWord("");
  };

  return (
    <>
      <h1>React Hooks: useMemo</h1>
      <div>
        <UnsortedWords words={words} />
        <SortedWords words={words} />
      </div>
      <input
        value={word}
        onChange={({ target: { value } }) => setWord(value)}
        placeholder="word"
      />
      <button onClick={handleClick}>+</button>
    </>
  );
}

다음으로 <App/> 컴포넌트에서 사용되는 <SortedWords/> 컴포넌트를 작성하겠습니다. 이 컴포넌트는 단어 배열(words)를 prop으로 받고, 배열 내 단어들을 알파벳 순으로 정렬한 후에, <ul/> 엘리먼트에 감싸 출력합니다.

여기서 주목해서 봐야 할 부분이 바로 정렬을 담당하고 있는 sortWords 함수인데요. 일부로 느린 함수를 시뮬레이션하기 위해서 구현한 delay 함수를 호출하여 일부로 sortWords 함수의 실행에 0.5초의 지연을 주고 있습니다.

import React from "react";

function SortedWords({ words }) {
  const sortWords = () => {
    console.log("sortWords");
    delay(500);
    return words.sort();
  };

  const sortedWords = sortWords(); // SLOW

  return (
    <>
      <h2>Sorted Words</h2>
      <ul>
        {sortedWords.map((word, idx) => (
          <li key={idx}>{word}</li>
        ))}
      </ul>
    </>
  );
}

function delay(ms) {
  const now = new Date().getTime();
  while (new Date().getTime() < now + ms) {}
}

자, 이제 앱을 실행시키고 입력 필드에 단어를 입력하면 어떤 현상이 일어날까요? 글자를 하나 하나 입력할 때마다, 지연이 발생해서, 앱을 사용하기 매우 불편하다는 것을 느끼실 수 있으실 것입니다.

그 이유는 한 글자 한 글자 입력할 때마다 <input/> 엘리먼트에서는 change 이벤트가 발생해서, word 상태가 계속 업데이트되기 때문입니다. 따라서 매 글자가 입력될 때 마다, <App/> 컴포넌트는 재 랜더링이 되야하고, 자식인 <SortedWords/> 컴포넌트도 함께 재 랜더링이 되야합니다. (콘솔을 확인해보면, 글자를 입력할 때 마다 sortWords가 출력되는 것이 확인되실 겁니다.) 결국, 처리 속도가 느린 sortWords 함수가 반복적으로 호출되면서, 앱의 성능이 현저히 떨어지게 되는 것입니다.

게다가, <SortedWords/> 컴포넌트의 prop인 words는 사용자가 버튼을 클릭할 때까지 계속 동일한 값이 넘어오는 것을 감안하면, sortWords 함수는 동일한 인자에 대해 불필요하게 여러 번 호출되고 있고 앱의 성능 저하의 주범이 되는 것이지요.

지금까지 작성한 실습 코드는 아래에서 확인할 수 있으며, 앱도 직접 실행해보실 수 있으십니다. (<UnsortedWords/> 컴포넌트는 별다른 내용이 없어서 언급하지 않았으니 여기서 확인 바랍니다.)

[실습] useMemo 적용

이제, <SortedWords/> 컴포넌트에 useMemo hook 함수를 적용하여 성능을 향상시켜 보겠습니다. 기존에 바로 호출하던 sortWords 함수를 useMemo 함수의 첫번째 인자로 넘기고, 두번째 인자로 words prop이 든 배열을 넘김니다. 이렇게 해주면, sortWords 함수는 words prop이 달라졌을 때만 호출이 되고, words prop이 동일할 때는 최초 호출 결과가 계속해서 재사용됩니다.

import React, { useMemo } from "react";

function SortedWords({ words }) {
  const sortWords = () => {
    console.log("sortWords");
    delay(500);
    return words.sort();
  };

  const sortedWords = useMemo(sortWords, [words]); // FAST

  return; /* 생략 */
}

다시 앱을 실행해보면, 입력 필드에 글자를 입력할 때마다 지연이 발생하지 않는 것을 확인하실 수 있으실 겁니다. 대신에 버튼을 클릭하면, sortWords 함수가 호출되어 의도했던 지연이 발생하게 되는 것을 콘솔 로그로도 확인하실 수 있으실 겁니다.

마치면서

지금까지 memoization 기법을 함수형 컴포넌트에 쉽게 적용할 수 있도록 도와주는 React의 useMemo hook 함수에 대해서 알아보았습니다.

일반적으로 소프트웨어의 성능 최적화에는 그에 상응하는 대가가 따르기 마련입니다. 따라서, 성능 최적화를 할때는 얻을 수 있는 실제 성능 이점이 지불하는 대가에 비해서 미미하지 않은지에 대해서 반드시 따져보시고 사용을 하셔야합니다.

예를 들어, useMemo hook 함수를 납용하면, 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어려워지고 유지보수성도 떨어지게 됩니다. 또한 useMemo가 적용된 레퍼런스는 재활용을 위해서 가바지 컬렉션(garbage collection)에서 제외되기 때문에 메모리를 더 쓰게 됩니다.

그래서 여태까지 제 개인적인 경험으로는 실제 웹 프로젝트에서 useMemo hook 함수를 사용할 일은 생각보다 그렇게 많지가 았았습니다. 왜냐하면 수초 이상 걸리는 로직이 프론트엔드(front-end)에 존재하다는 것 자체가 일반적인 앱에서는 흔치 않은 일이고, 설사, 그렇게 오래 걸리는 로직이 있다고 해도 useEffect hook 함수 등을 이용해서 비동기로 처리하는 방안을 먼저 고려하게 되기 때문입니다. 따라서 useMemo가 빛을 발휘할 수 있는 상황은 극히 제한적이며, 브라우저에서 React가 실행되는 속도도 워낙 빠르다보니 왠만한 컴포넌트가 여러 번 랜더링이 일어난다고 해서 실제 심각한 성능 이슈로 이어지는 경우는 의외로 적은 것 같습니다.

그럼에도 불구하고 최근들어 무분별하게 useMemo를 사용하려는 개발자들를 우연히 많이 접하게 되었고, 그래서 useMemo hook 함수에 대해서 한 번 다루고 싶어지게 되어 이렇게 포스팅을 하게 되었네요.

React Hooks 관련 포스팅