[FE] 메모이제이션(Memoization)

seunghee.Rho·2025년 9월 19일

FE

목록 보기
26/26

메모이제이션(Memoization)이란?

메모이제이션은 성능을 최적화하기 위한 핵심 기술 중 하나이다.
동일한 입력 값으로 함수를 호출 했을 때, 이전에 계산했던 결과를 기억해뒀다가 다시 계산하지 않고 즉시 반환하는 방식이다.

함수의 입력 값을 키(key)로, 계산된 결과를 값(value)으로 하는 캐시(Cache)를 사용한다.

계산 비용이 높은 함수, 즉 순수함수에 적용할 때 가장 효과적이다.
순수함수는 동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부 상태를 변경하지 않는 함수를 말한다.

장점은 중복 계산을 피하여 애플리케이션의 실행 속도를 크게 향상시킬 수 있다는 점이다.

lodash.memoize

lodash 라이브러리의 memoize는 메모이제이션 개념을 자바스크립트에서 쉽게 구현할 수 있도록 도와주는 유틸리티 함수이다.

memoize는 함수를 인자로 받아서, 메모이제이션이 적용된 새로운 함수를 반환한다.

import memoize from 'lodash/memoize';

// 계산에 시간이 걸리는 가상의 함수
const heavyTask = (n) => {
  console.log(`[계산 중...] 입력값: ${n}`);
  // 복잡한 계산을 시뮬레이션
  return n * n;
};

// heavyTask 함수에 메모이제이션 적용
const memoizedTask = memoize(heavyTask);

console.log(memoizedTask(2)); // "[계산 중...] 입력값: 2" 출력 후, 4 반환
console.log(memoizedTask(2)); // 계산 과정 없이 바로 4 반환
console.log(memoizedTask(2)); // 계산 과정 없이 바로 4 반환

console.log('--- 다른 값 입력 ---');

console.log(memoizedTask(5)); // "[계산 중...] 입력값: 5" 출력 후, 25 반환
console.log(memoizedTask(5)); // 계산 과정 없이 바로 25 반환
  1. 메모이제이션 할 함수(heavyTask)를 memoize()에 전달하여 새로운 함수(memoizedTask)를 만든다.
  2. memoizedTask(2)를 처음 호출하면, heavyTask(2)가 실행되고 그 결과(4)가 캐시에 저장된다. ({'2': 4})
  3. memoizedTask(2)를 다시 호출하면, heavyTask를 실행하지 않고 캐시에 저장된 값 4를 즉시 반환한다.

React의 useMemo와 useCallback

React 컴포넌트의 상태(state)나 props가 변경될 때마다 리렌더링이 발생한다.
이때 컴포넌트의 내부의 함수나 변수도 다시 계산되거나 생성되는데, 이것이 성능저하의 원인이 될 수 있다.

useMemo와 useCallback은 바로 이런 불필요한 작업을 막기 위한 React hook이다.

useMemo: 값(Value)을 메모이제이션

useMemo는 계산 비용이 큰 연산의 결과값을 메모이제이션(기억)하기 위해 사용된다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

두번째 인자로 전달된 의존성 배열의 값이 변경될 때만 첫 번째 인자인 콜백함수를 실행한다.
의존성 배열의 값이 같다면, 이전 리렌더링에서 계산했던 값을 그대로 재사용한다.

주로 큰 배열을 필터링, 정렬, 매핑하는 등 시간이 오래 걸리는 연산 결과나,
props를 기반으로 복잡한 계산을 통해 만들어지는 값에 사용한다.

useCallback: 함수(Function)를 메모이제이션

useCallback은 함수 자체를 메모이제이션하기 위해 사용된다.

const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

useMemo와 유사하게, 의존성 배열의 값이 변경될 때만 새로운 함수를 생성한다.
의존성 배열의 값이 같다면, 이전 리렌더링 때 사용했던 함수의 참조(주소값)를 그대로 재사용한다.

자식 컴포넌트에 props로 함수를 전달할 때. useCallback을 사용하지 않으면 부모가 리렌더링될 때마다 새로운 함수가 생성되어 자식 컴포넌트도 불필요하게 리렌더링될 수 있다.

React Compiler

https://ko.react.dev/learn/react-compiler#usage-with-vite

React Compiler는 React의 성능을 자동으로 최적화해주는 도구이다.
이 컴파일러는 개발자가 useMemo, useCallback, React.memo와 같은 최적와 API를 코드에 직접 작성하지 않아도, 빌드 과정에서 코드를 분석하여 리렌더링이 불필요한 부분들을 알아서 메모이제이션 처리한다.

React의 렌더링 모델은 UI를 일반적인 JS 값으로 다룬다. 하지만 이 때문에 상태가 변경되면 React는 어떤 부분이 바뀌었는지 확인하기 위해 관련 컴포넌트들을 다시 렌더링하는 경향이 있다.

React Compiler는 JS 규칙과 React 규칙을 모두 이해하는 최적화 컴파일러다.
코드를 분석하여 어떤 부분을 안전하게 건너뛰고(메모이제이션) 리렌더링할지, 또는 UI를 부분적으로만 업데이트할 수 있는지 등을 자동으로 파악한다.

자동 메모이제이션: 컴파일러가 코드의 의존성을 분석하여, useMemo와 useCallback을 사용해야 할 곳을 찾아 자동으로 적용한다.
성능 향상: 불필요한 리렌더링을 최소화하여 애플리케이션의 전반적인 성능과 반응성을 크게 향상시킨다.
코드 간소화: 개발자가 직접 최적화 훅을 관리할 필요가 없어지므로 코드가 훨씬 깔끔하고 직관적으로 변한다.


그럼 기존 프로젝트에서 useMemo, useCallback을 모두 제거하고 react compiler를 도입하면 되는 거 아닌가 ??

=> 이론적으로는 맞다. 컴파일러의 궁극적인 목표는 개발자가 수동 최적화 코드를 직접 작성하지 않아도 되게 하는 것이기 때문, 따라서 컴파일러를 도입하면 대부분의 useMemo와 useCallback을 제거할 수 있다.

하지만, 컴파일러가 만능은 아니기 때문에 코드의 모든 복잡한 패턴이나 잠재적인 사이드 이펙트를 100% 분석하지 못할 수도 있다. 동적이거나 예측하기 어려운 로직에서는 컴파일러가 의도치 않게 건너뛸 수도 있다...

결론: 점진적인 도입을 하자, 한번에 모두 제거하면, 예상치 못한 버그가 발생할 수 있음
컴파일러가 최적화를 잘 수행하는지 성능 프로파일링을 통해 확인하자 !!!! 해봐야지 ..

profile
Web Developer

0개의 댓글