해당 포스트는 디바운싱, 쓰로틀링, 메모이제이션에 대한 개념과 직접 구현해본 예시를 작성한 포스트입니다.
😶🌫️ 디바운싱 ( Debouncing )
연속적인 요청중에 가장 마지막 요청만 받는 기법입니다.
아래는 디바운싱 헬퍼 함수를 직접 구현해봤습니다.
1
2
3
4
5
6
7
8
9
10
11
12
// 연속적인 요청중에 가장 마지막 요청만 유효하게 적용 ( 고차 함수 )
const debouncingHelper = (func, wait) => {
let timerId = null;
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
func(...args);
}, wait);
};
};
저의 경험으로는 검색창의 추천 검색어를 패치하는 경우 사용한 경험이 있습니다.
만약 나의 해방일지
라고 검색한다면 ㄴ
, 나
, 나ㅇ
, 나으
, 나의
… 이렇게 하나하나 입력할 때마다 추천 검색어 요청을 보내게 되면 쓸데없는 낭비가 발생합니다.
저희는 나의
정도를 입력한 경우에 추천 검색어를 요청하면 적당하게 값을 패치받을 수 있기 때문에 한글자씩 보다는 다른 조건을 통해서 추천 검색어를 패치받으면 좋습니다.
그 기준을 “입력 발생이 0.5초동안 일어나지 않으면 -> 추천 검색어를 요청해줘!”라는 의미로 입력 이벤트에 디바운싱을 적용하면 네트워크 비용도 줄일 수 있고 사용자 경험도 좋은 기능을 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
const sum = (x, y) => console.log(x + y);
const debounceSum = debouncingHelper(sum, 1000);
// 연속적인 요청
debounceSum(1, 1);
debounceSum(2, 2);
debounceSum(3, 3);
debounceSum(4, 4);
debounceSum(5, 5); // 10 -> 마지막 요청만 실행
🫣 쓰로틀링 ( Throttling )
요청을 받은 후 정해진 시간동안 다른 요청을 무시하는 기법입니다.
아래는 쓰로틀링 헬퍼 함수를 직접 구현해봤습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 정해진 시간동안 다른 요청 모두 무시 ( 고차 함수 )
const throttleHelper = (func, wait) => {
// 자유 변수
let timerId = null;
// "args"도 자유 변수들
return (...args) => {
// 타이머가 돌아 간다면 실행하지 않기 ( 쓰로틀링 )
if (timerId) return;
timerId = setTimeout(() => {
func(...args);
timerId = null;
}, wait);
};
}
저의 경험으로는 주로 스크롤 이벤트에 사용했습니다.
스크롤에 이벤트를 달아보면 알 수 있지만, 한 번의 스크롤에 수십번의 이벤트가 발생합니다.
만약 스크롤 이벤트를 이용해서 무한 스크롤링같은 기능을 구현한다면 같은 패치 요청을 수십번을 서버에게 보내서 불필요한 네트워크 비용이 다수 발생하게 됩니다.
이런 불필요한 낭비를 줄이기 위해서 스크롤 이벤트에 쓰로틀링을 적용하면 정해진 기간동안 스크롤 이벤트를 한 번만 받을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
const sum = (x, y) => console.log(x + y);
const throttleSum = throttleHelper(sum, 1000);
// 연속적인 요청
throttleSum(1, 1); // 2 -> 첫 번째 요청만 실행
throttleSum(2, 2);
throttleSum(3, 3);
throttleSum(4, 4);
throttleSum(5, 5);
📑 메모이제이션 ( Memoization )
“« 자바스크립트 완벽 가이드 8장 251p ~ 252p » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )”를 참고해서 만든 예시입니다.
0️⃣ 재귀 함수와 메모이제이션
이전에 실행했던 혹은 생성했던 데이터를 캐싱하고 이후에 같은 요청이 오는 경우 그대로 가져다가 사용하는 기법입니다.
제가 알기로는 React.js
의 useMemo()
, useCallback()
, memo()
등이 메모이제이션 기법을 이용해서 최적화하는 것으로 알고 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 메모이제이션 헬퍼 함수 ( 고차 함수 )
const memorize = (func) => {
// 자유 변수
const cache = {};
// 클로저
return (...args) => {
const key = args.join("|");
if (!cache.hasOwnProperty(key)) {
cache[key] = func.apply(this, args);
}
return cache[key];
};
};
직접 만들어본 메모이제이션 헬퍼 함수를 이용해서 재귀적인 피보나치 수열 계산 함수의 속도 차이를 비교해보겠습니다.
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
30
31
32
33
34
35
36
37
38
39
40
// 일반적인 피보나치 수열 ( 재귀 함수 )
const fibonacci = (n) => {
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
};
// 메모이제이션이 적용된 피보나치 수열
const memoFibonacci = memorize((n) => {
if (n === 0) return 0;
if (n === 1) return 1;
return memoFibonacci(n - 1) + memoFibonacci(n - 2);
});
// 0 1 1 2 3 5 8 13 21 34 55
console.time("일반");
console.log(fibonacci(40));
console.log(fibonacci(40));
console.log(fibonacci(40));
console.timeEnd("일반");
console.time("메모");
console.log(memoFibonacci(40));
console.log(memoFibonacci(40));
console.log(memoFibonacci(40));
console.timeEnd("메모");
/**
* 102334155
* 102334155
* 102334155
* 일반: 3.226s
* 102334155
* 102334155
* 102334155
* 메모: 0.316ms
*/
제 컴퓨터 기준으로 위와 같은 성능 차이가 발생합니다.
복잡하고 오래걸리는 연산에 사용하면 성능을 많이 향상시킬 수 있는 좋은 기법인 것 같습니다.
1️⃣ 재귀 함수와 메모이제이션 사용 시 주의할 점
단, 재귀 함수를 메모이제이션하는 경우에는 아래와 같이 사용하면 제대로 메모이제이션이 되지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
// (1)
const fibonacci = (n) => {
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
};
// (2)
const memoFibonacci = memorize(fibonacci);
console.log(memoFibonacci(40));
(2)
처럼 사용하는 경우 최초 요청만 memoFibonacci()
를 사용하고 그 이후부터는 fibonacci()
를 사용합니다.
따라서 결과적으로 캐싱되는 값은 fibonacci(40)
만 남게 됩니다.
📮 레퍼런스
- 메모이제이션: « 자바스크립트 완벽 가이드 8장 251p ~ 252p » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )