자바스크립트 최고의 입문서로 꼽히는 모던 자바스크립트 Deep Dive를 보면 책 끄트머리 46장에 제너레이터 함수에 대해 다루고 있다.
책에 의하면 제너레이터는 ES6에 도입된 함수로, 코드 블록의 실행을 중단했다가 필요한 시점에 재개할 수 있는 특수한 함수라고 한다.
그런데 이 책에서 본 설명 이후로 아직까지 제너레이터를 실제로 쓰는 경우를 접하지 못했다. 분명 누군가의 문제를 해결하기 위해 도입되었을텐데 대체 언제 제너레이터를 쓰는걸까?
문제상황
1
2
3
for (let i = 0; i < 100000000; i++) {
array.push(i);
}
사실 이 배열에는 1억개의 비즈니스 데이터를 전처리하는 과정이라고 하자 이 배열의 크기는 적어도 800MB 정도로 꽤 클 것이다. (Number 타입 데이터는 8byte이므로)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const startTime = Date.now();
const array = [];
for (let i = 0; i < 100000000; i++) {
array.push(i);
}
console.log(`${Date.now() - startTime} ms: 작업을 시작합니다.`);
let count = 0;
for (let item of array) {
count++;
for (let i = 0; i < 10; i++) {
item = item * 10;
item = item / 10;
}
if (count % 20000000 === 0) {
console.log(`${Date.now() - startTime} ms: 진척도가 20% 증가했습니다.`);
}
}
console.log(`${Date.now() - startTime} ms: 작업이 완료되었습니다.`);
위 과정은 1억개의 데이터를 DB에서 불러와서 정말정말 작업을 시작하여 진척도를 모니터링하며 수행하는 코드이다. (라고 가정하자)
1
2
3
4
5
6
7
2013 ms: 작업을 시작합니다.
2868 ms: 진척도가 20% 증가했습니다.
3421 ms: 진척도가 20% 증가했습니다.
3970 ms: 진척도가 20% 증가했습니다.
4550 ms: 진척도가 20% 증가했습니다.
5120 ms: 진척도가 20% 증가했습니다.
5121 ms: 작업이 완료되었습니다.
메모리 사용량 약 2GB
로그에서 보이듯, 데이터 생성 과정을 모두 거쳐야만 작업을 시작할 수 있게 된다. 또한, 이 과정에서 기존에 불러온 데이터를 모두 메모리에 담고 있어야 해서 많은 메모리가 필요했다.
이 경우 다음과 같은 문제가 있을 수 있다.
- 만약 데이터가 더 크거나, 컴퓨터의 메모리가 부족하다면 작업 수행이 많이 어려워질 것이다.
- 만일 작업 결과를 가지고 또 다음 로직을 처리해야 한다면 그만큼의 대기시간이 또 생길 것이다.
제너레이터 사용
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
const startTime = Date.now();
// 제너레이터 함수 사용
function* gen() {
for (let i = 0; i < 100000000; i++) {
yield i;
}
}
const items = gen();
console.log(`${Date.now() - startTime} ms: 작업을 시작합니다.`);
let count = 0;
for (let item of items) {
count++;
for (let i = 0; i < 10; i++) {
item = item * 10;
item = item / 10;
}
if (count % 20000000 === 0) {
console.log(`${Date.now() - startTime} ms: 진척도가 20% 증가했습니다.`);
}
}
console.log(`${Date.now() - startTime} ms: 작업이 완료되었습니다.`);
기존에 array
를 만들어 반환하는 함수를 제너레이터로 바꾸어 주었다.
이 코드는 다음과 같이 동작한다.
- 제너레이터 객체가
items
에 할당 for ... of
반복문 진입gen()
제너레이터의 yield문 까지 코드가 진행, yield 표현식의 결과가 반복문의item
에 할당item
을 갖고 반복문 로직 수행, 다음 반복 진입- 반복 1억회가 끝날 때까지 3 ~ 4를 반복
즉 이 경우 항상 하나의 요소만 메모리에 존재하게 된다. 기존 방법은 1억개를 모두 메모리에 담고 있어야 했다.
즉 어마어마한 메모리 절약 효과를 볼 수 있다.
1
2
3
4
5
6
7
0 ms: 작업을 시작합니다.
813 ms: 진척도가 20% 증가했습니다.
1618 ms: 진척도가 20% 증가했습니다.
2383 ms: 진척도가 20% 증가했습니다.
3165 ms: 진척도가 20% 증가했습니다.
3958 ms: 진척도가 20% 증가했습니다.
3958 ms: 작업이 완료되었습니다.
**메모리 사용량 약 40MB **
이제는 제너레이터 객체가 생성 되는 즉시 반복문이 실행되는 것을 확인할 수 있다. 또한 2GB나 필요했던 기존의 프로그램과 다르게 메모리 사용량이 40MB로 매우 크게 감소하였다!
로그에는 실행 속도까지 빨라진 것을 볼 수 있는데 이는 제너레이터 함수가 너무 단순했던 것이 큰 것 같다. 실제로는 오버헤드 등의 문제로 인해 성능에는 다소 악영향을 줄 수 있다고 한다.
다만 대기시간 없이 즉시 실행되기 때문에 즉각적인 피드백이 중요한 도메인에서는 효과적으로 활용할 수 있다.
결론
제너레이터 함수는 반복에 필요한 데이터가 몇 개든 상관없이 항상 하나의 데이터만 필요하다는 장점을 갖고 있어, 메모리가 많이 필요한 대용량 데이터 처리에서 효율적으로 사용할 수 있다.
또한 이터러블이기에 [...items]
와 같이 배열로 바꾸어서 사용할 수도 있다. 단 이 경우, 모든 평가가 끝난 뒤에 배열이 생성되므로 사실상 제너레이터의 이점을 모두 잃어버리기 때문에 권장하지 않는다고 한다.
당장 쓸일이 있을지는 모르겠지만, 유용한 지식을 하나 배운 것 같다.