컴퓨팅 에서 메모이제이션 또는 메모이제이션은 비용이 많이 드는 함수 호출 의 결과를 저장 하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램 속도를 높이는 데 주로 사용되는 최적화 기술 입니다.
위키피디아 참조
이전에 계산한 값을 메모리에 저장하고, 그 값을 반복되는 연산에서 사용함으로써 중복되는 계산을 줄일 수 있습니다.
memoization에서 가장 흔하게 사용되는 예시는 피보나치 수열입니다.
피보나치 수열을 구하는 재귀함수를 fib라고 하고, 5번째 값(fib(5))을 구해보겠습니다.
아래의 사진과 같이 15번 호출되는걸 알 수 있습니다.
그러나 fib(3)은 2번, fib(2)는 3번, fib(1)은 5번, fib(0)은 3번이 중복되어 계산이 됩니다. 총 15번의 계산 중 무려 11번을 중복해서 계산하게 됩니다.
만약 이미 계산한 값을 재활용한다면 어떻게 될까요??
아래의 사진과 같이 총 9번만 계산하면 됩니다. 만약 값이 5가 아닌 엄청 크게 된다면 시간복잡도가 엄청나게 증가하고, 성능적으로 문제가 발생할 수 있습니다.
이처럼 하위 문제에 대한 정답을 계산했는지 확인해가며 하향식으로 문제를 자연스럽게 풀어나가는 방식을 메모이제이션(Memoization) 이라고 합니다.
기본형태 : const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
: 캐시하려는 값을 계산하는 함수입니다. ()=>{} 형태
순수해야 하고 인수를 사용하지 않아야 하며 모든 유형의 값을 반환해야 합니다. React는 초기 렌더링 중에 함수를 호출합니다.
dependencies
: 어떤 값이 변경되었을 때 다시 연산해야 할지 알려주기 위한 배열을 넣어줍니다. 이를 dependencies array, 줄여서 deps라고 합니다.
Ex) 아래의 코드는 value1, value2 즉 2가지 state값이 있고, 각 값을 1씩 증가시키는 버튼이 각각 존재합니다.
이때 버튼2를 클릭하면 value2라는 state값이 변화되고, state값이 변화함에 따라 re-rendering이 발생하고, re-rendering이 발생함에 따라 multiple()함수는 다시 연산이 됩니다.
import { useState } from 'react';
import MainPage from '../components/pages/MainPage';
const Average = () => {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const handleClick1 = () => {
setValue1((prev) => prev + 1);
console.log('1번 버튼 클릭');
};
const handleClick2 = () => {
setValue2((prev) => prev + 1);
console.log('2번 버튼 클릭');
};
const multiple = () => {
const mul = value1 * value1;
console.log('복잡한 계산이 수행되었습니다', mul);
return mul;
};
return (
<MainPage>
<div>
<p>{multiple()}</p>
<p>{value2}</p>
<button type="button" onClick={handleClick1}>
value1 +1
</button>
<button type="button" onClick={handleClick2}>
value2 +1
</button>
</div>
</MainPage>
);
};
export default Average;
복잡한 계산(multiple 함수)이 수행되는 걸 알 수 있습니다.
const multiple = useMemo(() => {
const sum = value1 * value1;
console.log('복잡한 계산이 수행되었습니다', sum);
return sum;
}, [value1]);
복잡한 계산이 수행되지 않는 걸 확인할 수 있습니다.
useCallback도 useMemo와 상당히 비슷한 함수입니다. 주로 렌더링 최적화에 사용하며 사용법도 아주 비슷합니다.
Ex) const cachedFn = useCallback(fn, dependencies)
그러나 차이점은 useMemo는 값을 메모이제이션
하고 useCallback은 함수 자체를 메모이제이션 합니다.
그렇다면 항상 useMemo 혹은 useCallback을 사용하면 될까? => 당연히 안됩니다.
왜 why? 메모이제이션이라는게 특정 메모리에 값을 저장
하는것입니다. 즉 비용이 발생하는데 단순 연산처리가 메모이제이션으로 저장하는 비용보다 더 저렴한 경우, 메모이제이션 하는 경우가 더 낭비일 수 있기 때문입니다.
아래의 두 코드에서 두번째 렌더시에 useCallback을 사용하지 않은 경우
dispense 함수가 garbage collected(메모리 공간을 없애고)가 되고, 새로운것이 생성됩니다.
하지만 useCallback을 사용한 경우
원본 dispense 함수는 garbage collected 되지 않고, 메모리 관점에서 더 좋지 않은 결과를 얻게 되는 것입니다. 삭제되지 않고 계속 새로운 메모리에 공간을 만들기 때문
// useCallback 사용 O
const dispense = React.useCallback(candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])
// useCallback 사용 X
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy));
};
자바스크립트의 객체는 같은 값이라도 다른 참조를 하고 있기 때문에 {a}==={a}
의 결과는 false가 나타나게 됩니다.
아래의 코드에서 useEffect는 이전 렌더링의 options의 참조값과 비교해 다르다면 useEffect의 콜백함수가 실행되는데, options 변수는 렌더링 될때마다 새로운 참조값을 가지게 되기 때문에 useEffect callback 함수는 bar나 baz가 변화할때마다 불리는 것이 아니라 매 렌더마다 불리게 되는것입니다
function Foo({ bar, baz }) {
const options = { bar, baz };
React.useEffect(() => {
buzz(options);
}, [options]);
return <div>foobar</div>;
}
function Blub() {
const bar = () => {};
const baz = [1, 2, 3];
return <Foo bar={bar} baz={baz} />;
}
function Foo({ bar, baz }) {
React.useEffect(() => {
const options = { bar, baz };
buzz(options);
}, [bar, baz]);
return <div>foobar</div>;
}
function Blub() {
const bar = React.useCallback(() => {}, []);
const baz = React.useMemo(() => [1, 2, 3], []);
return <Foo bar={bar} baz={baz} />;
}
const calcValue = () => {
let value = value1;
for (let i = 0; i < 10000; i++) {
for (let j = 0; j < 10000; j++) {
value = value + i + j;
}
}
return value;
};
const computedMemoValue = useMemo(calcValue, [value1]);