Post

메모이제이션과 리액트 memo

메모이제이션 개념 정리 및 리액트 memo 알아보기

메모이제이션이 뭐지?

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 메모이제이션을 글자 그대로 해석하면 ‘메모리에 넣기’라는 의미이다

- 위키백과 -

메모이제이션은 리액트에서만 사용되는 개념이 아닌, 프로그래밍에 널리 사용되는 기술이다.

Q. 왜 메모라이제이션이라고 안하고 메모이제이션이라고 하나요?

A. “메모이제이션”은 단순히 무언가를 기억하는 “암기”와는 다르며, 특정한 목적(성능 향상)으로 무언가를 기억하는 것을 의미하기 위해 만들어진 용어입니다.

캐싱과 비슷한건가?🤔

내가 이해한 바론, 캐싱과 같은 거라고 볼 수 있다. 다만, 캐싱은 접근 속도를 개선하기 위해 무언가를 저장하는 모든 것에 대한 포괄적인 개념이라면 메모이제이션은 함수와 관련된 캐싱이라고 볼 수 있다.

여기 에서는 이렇게 설명한다.

메모이제이션은 실제로 특정 유형의 캐싱입니다. 캐싱이라는 용어는 일반적으로 나중에 사용할 수 있는 모든 저장 기술(예: HTTP 캐싱)을 지칭할 수 있지만 메모이제이션은 값을 반환하는 캐싱 함수를 보다 구체적으로 지칭합니다.

스택 오버플로우에서도 동일한 질문이 있는데, 답변은 다음과 같다.

메모이제이션 은 함수의 매개변수에 따라 함수의 반환 값을 캐싱 하는 특정 형태의 캐싱입니다 .

따라서, 메모이제이션은 캐싱이긴한데 함수의 반환 값을 캐싱하는 것이다.

메모이제이션 구현 예제

어떤 식으로 반환 값을 캐싱한다는 건지 좀 더 이해해보자. 예를 들어, 다음과 같은 add 함수가 있다.

1
2
3
function add(a, b) {
  return a + b;
}

다음처럼 같은 매개변수 1, 2를 전달하여 함수를 여러 번 호출한다고 치자.

1
2
3
add(1, 2);
add(1, 2);
add(1, 2);

원래라면 매번 1+2 연산을 할 것이다. 그런데 이걸 메모이제이션 한다면?

add(1+2) 을 한 번 호출하여 결과(3)를 어딘가에 저장한 후, 이후에 또 다시 add(1+2)가 호출될 때 더하기 연산을 하는 것이 아니라 이미 저장된 결과값인 3을 바로 반환하는 것이다.

다음은 메모이제이션을 구현하는 간단한 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function memoize(fn) {
  const cache = new Map(); // 결과를 저장할 Map 객체 생성

  return function (...args) {
    const key = `${a},${b}`; // 인자를 문자열로 변환하여 캐시의 키로 사용

    if (cache.has(key)) {
      return cache.get(key); // 캐시에 결과가 있으면 반환
    }

    const result = fn(...args); // 결과를 계산
    cache.set(key, result); // 결과를 캐시에 저장
    return result; // 계산된 결과 반환
  };
}

// 원본 함수
function add(a, b) {
  return a + b;
}

// 메모이제이션된 함수 생성
const memoizedAdd = memoize(add);

// 사용 예시
console.log(memoizedAdd(1, 2)); // 3
console.log(memoizedAdd(1, 2)); // 캐시된 결과: 3
console.log(memoizedAdd(2, 3)); // 5

이렇게 하면 동일한 인자로 함수가 호출될 때마다 계산을 반복하지 않고, 저장된 결과를 효율적으로 재사용할 수 있다.

React.memo: 컴포넌트 리렌더링 최적화

React.memo는 컴포넌트를 메모이제이션하여 불필요한 리렌더링을 방지하는 데 사용된다. 컴포넌트가 동일한 props를 받는 경우, React.memo는 해당 컴포넌트를 다시 렌더링하지 않고, 이전 렌더링 결과를 재사용한다.

말로만 봐서는 무슨 말인지 모르겠다. 아래 예제를 보면서 이해해보자.

memo: props가 변경될 때만 리렌더링

React는 일반적으로 부모가 리렌더링될 때마다 컴포넌트를 리렌더링한다. memo를 사용하면 부모가 리렌더링될 때 새로운 props가 이전 props와 동일하면 리렌더링 되지 않는 컴포넌트를 만들 수 있다. 이러한 컴포넌트를 memoized 상태라고 한다.

다음은 공식 문서에 있는 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { memo, useState } from "react";

export default function MyApp() {
  const [name, setName] = useState("");
  const [address, setAddress] = useState("");
  return (
    <>
      <label>
        Name{": "}
        <input value={name} onChange={(e) => setName(e.target.value)} />
      </label>
      <label>
        Address{": "}
        <input value={address} onChange={(e) => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return (
    <h3>
      Hello{name && ", "}
      {name}!
    </h3>
  );
});

Greeting 컴포넌트는 name이 변경될 때만 리렌더링되고, address가 변경될 때는 리렌더링 되지 않는다.

props 비교 방식

주의! prop가 객체, 배열 또는 함수인 경우 컴포넌트가 리렌더링됩니다.  memo는 배열, 객체 또는 함수와 같은 props가 전달되는 경우에 완전히 무용지물이다. 이 경우 useMemouseCallback 가 필요할 수 있다.

React는 얕은 비교를 기준으로 이전 props와 새로운 props를 비교한다.

1
2
3
4
const obj1 = { name: "clap" };
const obj2 = { name: "clap" };

console.log(obj1 === obj2); // false

여기서 obj1obj2의 내용은 동일하지만, 서로 다른 메모리 위치에 저장되어 있기 때문에 === 연산자는 두 객체가 동일하지 않다고 판단한다.

즉, 각각의 새로운 prop가 이전 prop와 참조가 동일한지 여부를 고려한다. 부모가 리렌더링 될 때마다 새로운 객체나 배열을 생성하면, 개별 요소들이 모두 동일하더라도 React는 여전히 변경된 것으로 간주한다. 마찬가지로 부모 컴포넌트를 렌더링할 때 새로운 함수를 만들면 React는 함수의 정의가 동일하더라도 변경된 것으로 간주한다.

이 경우 useCallbackuseMemo 가 필요할 수 있다.

This post is licensed under CC BY 4.0 by the author.