Web/React

[React] useMemo

동띵 2022. 12. 25. 22:22

useMemo는 컴포넌트 성능을 최적화하는 데 사용되는 리액트 훅 중 하나이다.

useMemo에서 Memo는 memoization을 뜻하는데,
이는 동일한 값을 리턴하는 함수를 반복적으로 호출해야 될 때
맨 처음 값을 메모리에 저장해서 필요할 때마다 재사용하는 기법이다.

 

useMemo는 처음 계산된 결괏값을 메모리에 저장해서
컴포넌트가 반복적으로 렌더링되어도 해당 함수를 계속 호출하지 않고,
메모리에 저장해둔 값을 재사용할 수 있게 해 준다.

 

useMemo는 두 개의 인자를 받는다.
첫 번째는 콜백함수, 두 번째는 배열이다.

// useMemo(콜백함수, 배열)
const value = useMemo(() => {
	return calculate();
}, [item]);

콜백함수가 리턴하는 값이 useMemo가 리턴하는 값이 된다.
위 코드 상에서 calculate 함수의 결괏값이 value가 되는 것이다.

 

두 번째 인자인 배열은 의존성 배열로, 해당 배열 요소 값이 업데이트될 때만
콜백함수를 다시 호출하여 memoization 된 값을 업데이트해서 다시 memoization을 한다.

만약 두 번째 인자로 빈 배열을 넣어주었다면
해당 컴포넌트가 처음 마운트됐을 때만 값을 계산하고, 이후에는 저장된 값을 재사용한다.

 

useMemo를 사용한다는 것은 재사용되는 값을 따로 저장하기 위해
메모리를 소비하는 것이므로 불필요한 값을 memoization 하면 성능이 악화될 수 있다.

import React, { useState } from 'react';

const hardCalculate = (num) => {
  console.log("어려운 계산");
  for (let i=0; i<999999; i++) {}
  return num + 10000;
}

const esayCalculate = (num) => {
  console.log("쉬운 계산");
  return num + 1;
}

function App() {
  const [hardNum, setHardNum] = useState(1);
  const [easyNum, setEasyNum] = useState(1);

  const hardSum = hardCalculate(hardNum);
  const easySum = esayCalculate(easyNum);

  return (
    <div>
      <h3>어려운 계산기</h3>
      <input
        type="number"
        value={hardNum}
        onChange={(e) => setHardNum(parseInt(e.target.value))}
      />
      <span> + 10000 = {hardSum}</span>

      <h3>쉬운 계산기</h3>
      <input
        type="number"
        value={easyNum}
        onChange={(e) => setEasyNum(parseInt(e.target.value))}
      />
      <span> + 1 = {easySum}</span>
    </div>
  );
}

export default App;

위 코드를 실행해보면 쉬운 계산기의 숫자만 바꿨는데
hardCalculate 함수도 실행되어 콘솔에 찍히는 것을 볼 수 있다. 

 

이것은 위 컴포넌트가 함수형 컴포넌트이기 때문이다.

state의 값이 바뀌면 해당 컴포넌트가 다시 렌더링 되는데,
이때 변수 hardSum과 easySum이 초기화되므로 hardCalculate 함수가 실행되는 것이다.

 

여기서 easyNum state를 변경할 때 hardCalculate 함수가 실행되지 않게 하려면 useMemo를 사용하면 된다.

 

useMemo는 어떠한 조건이 만족됐을 때만 특정 변수가 초기화되게 할 수 있다.

특정 조건에 만족하지 않으면 해당 컴포넌트가 렌더링 되어 내부 변수를 초기화하더라도
조건에 만족하지 않는 변수는 이전에 가지고 있던 값을 사용하는 것이다.
이것을 memoization이라고 한다.

const hardSum = useMemo(() => {
  return hardCalculate(hardNum)
}, [hardNum]);

기존 const hardSum = hardCalculate(hardNum)을 useMemo을 사용하여 바꿔주었다.

의존성 배열 부분에는 hardNum을 넣어 hardNum state가 바뀔 때만 hardCalculate 함수를 실행하게 했다.

그 결과 이전과는 다르게 쉬운 계산기의 숫자가 바뀌면
easyCalculate 함수만 실행되는 것을 확인할 수 있다.

반면 어려운 계산기의 숫자가 바뀌면 여전히 두 개의 함수가 실행되는 것을 볼 수 있다.

(easySum은 memoization을 하지 않았기 때문이다.)

 

또한 useMemo는 객체타입을 memoization 할 때 자주 사용된다.

import React, { useEffect, useState } from 'react';

function App() {
  const [num, setNum] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = {
    country: isKorea ? '한국' : '외국'
  };

  useEffect(() => {
    console.log('useEffect 호출');
  }, [location]);

  return (
    <div>
      <h2>하루에 몇 끼 먹어요?</h2>
      <input
        type="number"
        value={num}
        onChange={(e) => setNum(e.target.value)}
      />
      <hr/>
      <h2>어느 나라에 있어요?</h2>
      <p>나라: {location.country}</p>
      <button onClick={() => setIsKorea(!isKorea)}>비행기 타자</button>
    </div>
  );
}

export default App;

위 코드를 실행하면 location을 바꾸지 않아도 num state가 바뀔 때마다 useEffect가 실행된다.

그 이유는 location이 객체타입이기 때문이다.

 

객체타입은 변수에 메모리 상의 주소가 들어가 있어서
아무리 값이 같아도 주소가 달라 다른 변수이다.

따라서 num state를 변화시켰을 때 App 컴포넌트가 렌더링 되면서
location 변수가 재할당받게 된다.

그러면 다른 주소를 가진 location이 되므로 location이 바뀌어 useEffect가 실행되는 것이다.

 

이럴 때 useMemo를 사용하여 해당 컴포넌트가 렌더링 됐을 때
location 변수가 초기화되는 것을 막아줄 수 있다.

const location = useMemo(() => {
  return {
    country: isKorea ? '한국' : '외국'
  }
}, [isKorea]);

useMemo를 사용하여 location을 memoization 해주어
isKorea state가 바뀌었을 때만 초기화되게 해 주었다.

그러면 num state를 바꾸었을 때 useEffect가 실행되지 않는 것을 볼 수 있다.

 

따라서 불필요한 함수 실행을 줄이기 위해 useMemo를 사용하여
컴포넌트의 성능을 최적화할 수 있다.


참고자료