useMemo

김민기·2022년 12월 4일
0

React

목록 보기
6/8

memoization

useMemo를 알기전에 memoization에 대해서 간단하게 알아본다.

나무위키에서는 메모이제이션에 대해서 이렇게 설명하고 있다.

컴퓨터 프로그래밍 용어로, 동일한 계산을 반복해야 할 경우 한 번 계산한 결과를 메모리에 저장해 두었다가 꺼내 씀으로써 중복 계산을 방지할 수 있게 하는 기법이다. 동적 계획법의 핵심이 되는 기술로써 결국 메모리라는 공간 비용을 투입해 계산에 소요되는 시간 비용을 줄이는 방식이다. 메모아이제이션은 아무래도 학술적인 용어라 실제 현장에서는 캐싱(caching)이라는 단어를 더 많이 사용한다.

중요한 것은 동일한 계산을 반복해야할 경우 그 결과값을 메모리에 저장해 두었다가 필요할때 사용하면서 중복 계산을 방지한다는 것이다.

리액트에서 컴포넌트의 성능 최적화를 위해서 사용되는 훅은 대표적으로 useMemo와 useCallback이 있다.
useMemo가 어떻게 memoization을 통해서 성능 최적화를 만들 수 있는지 알아본다.

함수형 컴포넌트

리액트에서 자주 사용되는 함수형 컴포넌트 (훅을 사용한다는 것은 함수형 컴포넌트를 사용한다는 것이다.)는 말 그대로 함수다. 컴포넌트가 렌더링 된다는 것은 그 함수가 호출 된다는 것이다.

렌더링 될 때마다 함수가 호출되기 때문에 리렌더링이 발생한다면 함수 내부에서 사용하는 모든 변수들은 호출 될 때마다 초기화가 이루어진다.

function App() {
  const calculatedValue = calculate();
  
  return <div>{ value }</div>
}

function calculate() {
  return 10;
}

calculatedValue 라는 변수는 App 컴포넌트가 렌더링 될 때마다 calculate 함수를 호출해서 반환하는 값으로 초기화 된다. 지금은 간단한 함수이지만 만약 엄청나게 복잡하고 계산에 오랜 시간이 소요되는 로직이라면 App 컴포넌트가 렌더링 될 때마다 계산 때문에 오랜 시간이 소요되게 된다.

컴포넌트는 Props 또는 내부에서 사용하는 state가 변경될 때마다 렌더링된다. 따라서 리렌더링이 자주 발생할 가능성이 높다. 때문에 이런 반복적인 계산은 useMemo를 사용해서 memoization 해두는 것이 좋다.

useMemo

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

const value = useMemo(() => {
  return calculate();
}, [item]);

콜백 함수는 memoization 해줄 값을 계산해서 리턴해주는 함수다. 이 콜백 함수가 리턴하는 값이 바로 useMemo가 리턴하는 값이 된다.
의존정 배열은 배열 안의 요소가 변경될 때만 콜백 함수를 다시 호출해서 memoization된 값을 업데이트 하고 다시 memoization해준다. 빈 배열을 넘길 경우 컴포넌트가 마운트 될 때만 값을 계산하고 이후에는 항상 memoization된 값을 꺼내와서 사용한다.

import { useMemo, useState } from "react";

const hardCalculate = (number) => {
  console.log("Hard Calculate!");
  for (let i = 0; i < 999999999; i++) {}
  return number + 100000;
};

const easyCalculate = (number) => {
  console.log("Easy Calculate!");
  return number + 1;
};

function App() {
  console.log("App Rendering");
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  const hardSum = useMemo(() => {
    hardCalculate(hardNumber);
  }, [hardNumber]);
  const easySum = easyCalculate(easyNumber);

  return (
    <div>
      <h3>Hard Calculate</h3>
      <input
        type="number"
        value={hardNumber}
        onChange={(e) => setHardNumber(parseInt(e.target.value))}
      />
      <span> + 10000 = {hardSum}</span>

      <h3>Easy Calculate</h3>
      <input
        type="number"
        value={easyNumber}
        onChange={(e) => setEasyNumber(parseInt(e.target.value))}
      />
      <span> + 1 = {easySum}</span>
    </div>
  );
}

export default App;

이 코드에서 hardCalculate는 복잡한 계산을 의미하고 계산하는데 많은 시간이 소요된다. 반면 easyCalculate는 간단한 계산이고 짧은 시간안에 결과값을 출력한다. 만약 useMemo를 사용하지 않는다면 hardNumber가 변경되었을 때는 당연히 오랜 시간이 걸리는 것이 맞지만 easyNumber가 변경되었음에도 App 컴포넌트가 리렌더링 되면서 hardSum을 다시 초기화 하기 때문에 hardNumber를 변경하는 것 만큼 많은 시간이 소요된다.

useMemo를 사용해서 간단하게 이런 문제를 해결할 수 있다. hardNumber가 변경되었을 때만 hardSum을 다시 구하도록 만들었기 때문에 easyNumber가 변경된다고 해서 hardSum을 다시 계산할 필요가 없게 된다.

언제 사용해야나

지금까지 내용으로 본다면 컴포넌트는 리렌더링이 자주 발생하고, 그에 따라 내부에 있는 값들을 초기화 하기 때문에 useMemo를 사용해서 계산된 결과값을 메모리에 저장해두면 성능에 좋을 것이라 생각된다. 그렇기 때문에 모든 변수들에 대해서 useMemo를 사용한다면 컴포넌트의 성능이 매우 좋아지지 않을까?

하지만 오히려 성능에 무리가 갈 수 있다는 것을 알아야 한다. 값을 재사용한다는 것은 메모리를 사용해서 그 값을 기억한다는 것이고 그 값을 기억하기 위해서 메모리를 항상 소비해야 한다는 것이다. 사실상 그렇게 자주 사용되지 않는 값임에도 불구하고 메모리를 차지하고 있다면 오히려 성능에 악영향을 미친다.
따라서 꼭 필요할 경우에만 사용해야 한다.

객테 타입의 값

이렇게만 보면 useMemo를 사실상 언제 써야할지 또는 꼭 필요한지 의문이 든다. 복잡한 계산이라는 의미가 추상적이고 또 Memoization을 사용해서 메모리를 최적화 하는 것도 맞지만 그전에 로직을 개선해서 계산을 빠르게 할 수 있도록 알고리즘을 수정하는게 맞는 듯 하다. 또는 복잡해진 계산을 간단하게 만들 수 있는게 우선이라는 생각이 든다.

useMemo는 자바스크립트의 원시 타입 값이 아닌 객체 타입 값을 메모이제이션 해서 유용하게 사용할 수 있다.
간단하게 자바스크립트의 원시 타입 값과 객체 타입 값을 비교하면 변수에 원시 타입 값을 저장하면 변수에는 원시 타입 값이 할당되고 변수에 객체 타입 값을 저장하면 객체 타입 값을 저장하고 있는 메모리 주소가 할당된다. 따라서 다음과 같은 결과가 나온다.

const one = "one";
const two = "one";

one === two // true

const one = { number: "one" }
const two = { number: "two" }

one === two // false

객체 내부의 값이 동일 함에도 다르게 인식한다.

만약 useEffect를 사용한다고 해보자

const [number, setNumber] = useState(1);
const obj = { 
  number: number === 1 ? "one" : "null",
  type: "object" 
}
useEffect(() => {
  // something...
}, [obj]}

obj는 객체 타입의 값이고 useEffect는 obj가 변할 때마다 실행된다. 만약 이 컴포넌트가 동작한다면 obj를 수정하지 않았음에도 useEffect가 계속해서 실행되는 것을 볼 수 있다. 왜냐하면 컴포넌트가 렌더링 될 때마다 obj는 새롭게 초기화되는데 이때 obj라는 변수에 저장된 값은 객체를 저장하고 있는 메모리 주소다. 메모리 주소는 같을 수 없고 렌더링 될 때마다 바뀌기 때문에 useEffect는 obj에 변화가 있다고 감지하고 실행하게 된다.

따라서 obj를 메모이제이션 해두면 간단하게 해결할 수 있다.

const [number, setNumber] = useState(1);
const obj = useMemo(() => {
  return { 
    number === 1 ? "one" : "null",
    type: "object"
  }
}, [number])

useEffect(() => {
  // something...
}, [obj]}

obj는 number가 변경되었을 때만 새롭게 콜백함수를 실행하고 객체를 할당한다. 만약 number가 변경되지 않는다면 이전에 캐싱해둔 객체를 그대로 사용하기 때문에 useEffect가 실행되지 않는다.
이렇게 객체 타입의 값을 캐싱해둘때 유용하게 사용할 수 있다.

0개의 댓글