[TIL 2023.04.08] Memoization

김헤일리·2023년 4월 8일
0

TIL

목록 보기
44/46
post-custom-banner

개발을 공부하기 시작하면서 "메모이제이션" 이라는 단어를 자주 듣게 되었는데, 최적화에 관련된 내용이었다는 건 얼핏 알고 있었지만, 정확한 개념에 대해선 모르고 있었다.

특히 React의 경우, useCallbackuseMemo 가 메모이제이션을 지원하는 Hook이라고 하였는데, 사용을 해보려고해도 애초에 메모이제이션이 뭔지 잘 모르니 사용 방법에 대한 이해가 도저히 되지 않았었다.

메모이제이션은 뭐고 useCallbackuseMemo 는 어떻게 메모이제이션을 지원하는걸까?



1. Memoization

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

웹과 앱의 규모가 점점 커지면서 처리할 데이터의 양 또한 증가하였다. 더 많은 양의 컴퓨팅이 진행되다보니 로딩 속도를 줄이기 위한 최적화에도 당연히 관심이 쏠릴 수 밖에 없었다.

그래서 이전 데이터를 캐시하고 똑같은 입력이 다시 발생할 때 바로 반환하는 방식으로 어플리케이션의 구동 속도가 증가할 수 있었다.

메모이제이션이 얼마나 효과적인지 알려주는 예제는 피보나치 수열을 함수로 만드는 것이라고 한다.

피보나치 수열 함수!

function fibonacci(n) {
    if (n <= 1) {
        return 1
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

만약 fibonacci(5)의 값을 얻고싶다면, n의 값이 1이 될때가지 여러 분기가 생기면서 동일한 함수가 계속 실행된다.
각 분기점에서 새롭게 계산을 진행할 때 n의 값이 같은 함수가 여러개 생성되는데, 이렇게 중복 연산이 생기면 생길수록 더 많은 작업이 수행되고 최종 케이스에 도달하는데까지 시간이 많이 소비된다.

위와 같이 fib(3), fib(2), fib(1), fib(0) 함수들는 각 각 1번 이상이 실행되었다.
메모이제이션은 이런 상황에서 중복된 연산이 진행되지 않도록 한다고 한다.

피보나치 수열 함수 w/ Memoization

function fibonacci(n,memo) {
    memo = memo || {}
    if (memo[n]) {
        return memo[n]
    }
    if (n <= 1) {
        return 1
    }
    return memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
}

memo[n]의 값이 있는 경우, 이전에 memo에 캐시해뒀던 값을 리턴하게 된다. 캐시된 값을 리턴하기 때문에 함수는 더 이상 같은 인자에 대한 계산을 지속적으로 진행하지 않게 된다.

첫번째 방식의 함수와 비교했을 때, 특정 값을 캐시하고 추가적인 계산이 일어나지 않는다는 것이 어떤 의미인지 보다 더 파악할 수 있다.

메모이제이션은 성능을 최적화하기 때문에 굉장히 중요한 개념이라고 생각한다. 리액트에서도 성능 최적화를 위해 useMemo()useCallback()을 사용한다고 많이 들었지만, 애초에 개념에 대해 잘 몰랐기 때문에 설명이 와닿지 않았었다.

메모이제이션의 개념과 중요성에 대해 간단하게나마 알아봤으니 useMemo()useCallback()에 대해서도 간단하게 정리해야겠다.



2. React useMemo, useCallback

📌 useMemo

useMemo() 의 첫번째 파라미터에는 어떻게 연산할지 정의하는 함수를 넣어주면 되고 두번째 파라미터에는 deps 배열을 넣어주면 되는데, 이 배열 안에 넣은 내용이 바뀌면, 우리가 등록한 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.

import { useState } from "react";
 
const hardCalculate = (number) => {
  console.log("어려운 계산!");
  for (let i = 0; i < 99999999; i++) {} // 생각하는 시간
  return number + 10000;
};

const easyCalculate = (number) => {
  console.log("쉬운 계산!");
  return number + 1;
};

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);
  const easySum = easyCalculate(easyNumber);

  return (
    <div>
      <h3>어려운 계산기</h3>
      <input
        type="number"
        value={hardNumber}
        onChange={(e) => setHardNumber(parseInt(e.target.value))}
      />
      <span> + 10000 = {hardSum}</span>
      
      
      <h3>쉬운 계산기</h3>
      <input
        type="number"
        value={easyNumber}
        onChange={(e) => setEasyNumber(parseInt(e.target.value))}
      />
      <span> + 1 = {easySum}</span>
    </div>
  );
}

export default App;

이 예시에서 쉬운 계산기의 input의 값을 변경할 경우, App 컴포넌트 전체가 리렌더링 되기 때문에 hardCalculate()도 다시 실행된다. 굳이 리렌더링이 발생할 필요 없는 상황이기 때문에, hardCalculate()useMemo()로 감쌀 수 있다.

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

useMemo()의 첫 번째 인자로 콜백 함수인 hardCalculate()를 넣고, 의존성 배열 안엔 실행 조건인 hardNumber를 넣는다면, hardNumber가 변경될때만 콜백함수가 실행되고, 만약 hardNumber가 변하지 않았다면 이전에 캐시했던 값을 뱉어내는 것이다.


📌 useCallback

useCallback()useMemo()와 비슷하게 특정한 값을 재사용하고 싶을 때 사용한다. 다만, useCallback()은 특정 함수 자체를 새로 만들지 않고 재사용하기 위해 사용한다.

예를 들어 리액트 컴포넌트 안에 함수가 선언되어있을 때 이 함수는 해당 컴포넌트가 렌더링 될 때마다 새로운 함수가 생성되는데, useCallback을 사용하면 해당 컴포넌트가 렌더링 되더라도 그 함수가 의존하는 값(deps)들이 바뀌지 않는 한 기존 함수를 재사용할 수 있다.

함수를 생성하는 것 자체도 리소스가 드는 일인데, useCallback()을 사용하면, 원본 함수는 가비지 컬렉팅 되지 않고, 메모리에서 계속해서 한 공간을 차지함으로서 리소스를 세이브하게 되는 것이다.

function App() {
  const [name, setName] = useState('');
  const onSave = useCallback(() => {
    console.log(name);
  }, [name]);//name이 변경될 때에만 함수 재생성.

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}

위의 예시를 보자면 onSave() 함수는 App 컴포넌트에서 name이 변경되지 않는 이상 함수가 새로 만들어지지 않는다. 함수를 계속해서 사용할 수 있기 때문에 <Profile> 컴포넌트의 반복적인 리렌더링도 방지할 수 있다.



출처:

profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄
post-custom-banner

0개의 댓글