메모이제이션이란?
메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 실행 속도를 빠르게 하는 기술을 말한다.(캐시로 성능을 높이는 프로그램 기법)
알고리즘 문제를 풀 때 많이 사용되는 재귀함수(피보나치 수열)를 통해 메모이제이션을 알아보도록 하자.
피보나치 수열은 0과 1로 시작해서, 이전 두 개의 숫자를 더한 값을 다음 항으로 하는 수열을 말한다. 즉, 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, ...과 같은 형태를 가지며, 각 항은 그 이전 두 항을 더한 값으로 계산된다.
이 피보나치 수열을 재귀함수를 사용해서 함수를 만들면 아래와 같이 만들 수 있다.
const fibonacci = (n) => {
if ( n <= 1 ) {
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}
위 함수는 fibonacci 함수를 재귀적으로 수행하는데, 계산의 양이 늘어날 수록 계산의 중복으로 인해 시간복잡도는 증가하게 된다.
예를 들어 fibonacci(5)를 실행하면 fibonacci(4) + fibonacci(3) + … 순서로 실행하게 되는데, 여기서 fibonacci(4)는 다시 fibonacci(3) + fibonacci(2) + … 순서로 실행하게 된다. 이 과정 속에서 한 번 계산 했던 것을 반복하는 것을 볼 수 있다.
이때 한 번 계산한 결과를 저장해두고 거기서 값을 꺼내쓰면 계산의 중복이 발생하지 않게되어 효율적인 함수가 된다.
const fibonacci = (n,memo) => {
memo = memo || {}
if (memo[n]) {
return memo[n]
}
if (n <= 1) {
return n
}
return memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
}
이를 메모이제이션이라고 한다.
useMemo
useMemo는 컴포넌트 내부에서 계산된 값을 메모이제이션하여, 값이 변경되지 않았으면 이전의 값을 재사용하여 연산을 최적화 하는 React Hook이다.
// Average.tsx
import React, { useState, useRef } from "react";
const getAverage = (numbers: number[]) => {
console.log("평균값 계산중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a: number, b: number) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState<number[]>([]);
const [number, setNumber] = useState<string>("");
const inputEl = useRef<HTMLInputElement>(null);
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setNumber(e.target.value);
}, []);
const onInsert = () => {
const nextList = list.concat(Number(number));
setList(nextList);
setNumber("");
inputEl?.current?.focus();
};
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {getAverage(list)}
</div>
</div>
);
};
export default Average;
숫자를 등록하게 되면, 등록할 때뿐만 아니라 인풋 내용이 수정될 때도 getAverage 함수가 호출되는 것을 볼 수 있다. 평균값은 list가 변화될 때만 계산되야 하는데, input 값이 변경될 때마다 컴포넌트가 리렌더링 되면서 불필요하게 호출이 발생하게 된다.
useMemo Hook을 사용하면 이러한 작업을 최적화 할 수 있다. 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용하게 된다.
// Average.tsx
import React, { useState, useMemo, useRef } from "react";
const getAverage = (numbers: number[]) => {
console.log("평균값 계산중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a: number, b: number) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState<number[]>([]);
const [number, setNumber] = useState<string>("");
const inputEl = useRef<HTMLInputElement>(null);
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setNumber(e.target.value);
}, []);
const onInsert = () => {
const nextList = list.concat(Number(number));
setList(nextList);
setNumber("");
inputEl?.current?.focus();
};
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
코드를 위와 같이 작성해주면 list 배열의 내용이 바뀔 때만 getAverage 함수가 호출되는 것을 볼 수 있다.
useCallback
useCallback은 useMem와 비슷한 React Hook 이다. useMemo는 특정 결과값을 재사용할 때 사용하는 반면에, useCallback 주로 렌더링 성능을 최적화해야 하는 상황에서 사용한다. useCallback은 특정 함수를 새로 만들지 않고 만들어 놨던 함수를 재사용하고 싶을 때 사용한다.
위에 구현한 Average 컴포넌트에서, onChage와 onInsert 함수는 컴포넌트가 리렌더링 될 때마다 새로 만들어져 사용된다. 대부분의 경우 이러한 방식은 문제가 없지만, 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이 부분을 최적화해 주는 것이 좋다.
// Average.tsx
import React, { useState, useMemo, useRef, useCallback } from "react";
const getAverage = (numbers: number[]) => {
console.log("평균값 계산중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a: number, b: number) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState<number[]>([]);
const [number, setNumber] = useState<string>("");
const inputEl = useRef<HTMLInputElement>(null);
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setNumber(e.target.value);
}, []);
const onInsert = useCallback(() => {
const nextList = list.concat(Number(number));
setList(nextList);
setNumber("");
inputEl?.current?.focus();
}, [number, list]);
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수를 넣고, 두 번째 파라미터에는 배열을 넣어서 사용한다. 이 배열(Dependency Array - 의존성 배열)에는 어떤 값이 바뀌었을 때 함수를 새로 생성할지 명시해야 한다.
함수 내부에서 상태 값에 의존해야 하는 경우, 그 값을 반드시 두 번째 파라미터 안에 포함시켜야 한다. 예를들어 onInsert는 기존의 number와 list를 조회해서 nextList를 생성하기 때문에 배열 안에 number와 list를 넣어야 한다.
useCallback을 사용하는 경우
- 함수 자체를 캐싱해야할 때
- dependency를 확인해야 하는 함수인 경우
- 자식 컴포넌트에 prop으로 넘겨주는 함수인 경우
참고
https://ko.legacy.reactjs.org/docs/hooks-reference.html#usememo
https://react.vlpt.us/basic/17-useMemo.html
https://react.vlpt.us/basic/18-useCallback.html
https://jeonghwan-kim.github.io/2023/04/17/usememo-usecallback.
'React' 카테고리의 다른 글
렌더링?? (0) | 2023.07.07 |
---|---|
[React] React Router (0) | 2023.04.22 |
[React] input의 value 가져오기 (1) | 2023.04.18 |
Router (0) | 2022.07.11 |
State & Props (0) | 2022.07.05 |