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