[JS] 제너레이터는 언제 쓸까?

cjkangme·2023년 12월 27일
1

javascript

목록 보기
4/4
post-thumbnail
post-custom-banner

자바스크립트 최고의 입문서로 꼽히는 모던 자바스크립트 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

로그에서 보이듯, 데이터 생성 과정을 모두 거쳐야만 작업을 시작할 수 있게 된다.
또한, 이 과정에서 기존에 불러온 데이터를 모두 메모리에 담고 있어야 해서 많은 메모리가 필요했다.

이 경우 다음과 같은 문제가 있을 수 있다.

  1. 만약 데이터가 더 크거나, 컴퓨터의 메모리가 부족하다면 작업 수행이 많이 어려워질 것이다.
  2. 만일 작업 결과를 가지고 또 다음 로직을 처리해야 한다면 그만큼의 대기시간이 또 생길 것이다.

제너레이터 사용

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를 만들어 반환하는 함수를 제너레이터로 바꾸어 주었다.

이 코드는 다음과 같이 동작한다.

  1. 제너레이터 객체가 items에 할당
  2. for ... of 반복문 진입
  3. gen() 제너레이터의 yield문 까지 코드가 진행, yield 표현식의 결과가 반복문의 item에 할당
  4. item을 갖고 반복문 로직 수행, 다음 반복 진입
  5. 반복 1억회가 끝날 때까지 3 ~ 4를 반복

즉 이 경우 항상 하나의 요소만 메모리에 존재하게 된다.
기존 방법은 1억개를 모두 메모리에 담고 있어야 했다.

즉 어마어마한 메모리 절약 효과를 볼 수 있다.

0 ms: 작업을 시작합니다.
813 ms: 진척도가 20% 증가했습니다.
1618 ms: 진척도가 20% 증가했습니다.
2383 ms: 진척도가 20% 증가했습니다.
3165 ms: 진척도가 20% 증가했습니다.
3958 ms: 진척도가 20% 증가했습니다.
3958 ms: 작업이 완료되었습니다.

메모리 사용량 약 40MB

이제는 제너레이터 객체가 생성 되는 즉시 반복문이 실행되는 것을 확인할 수 있다.
또한 2GB나 필요했던 기존의 프로그램과 다르게 메모리 사용량이 40MB로 매우 크게 감소하였다!

로그에는 실행 속도까지 빨라진 것을 볼 수 있는데
이는 제너레이터 함수가 너무 단순했던 것이 큰 것 같다. 실제로는 오버헤드 등의 문제로 인해 성능에는 다소 악영향을 줄 수 있다고 한다.

다만 대기시간 없이 즉시 실행되기 때문에 즉각적인 피드백이 중요한 도메인에서는 효과적으로 활용할 수 있다.

결론

제너레이터 함수는 반복에 필요한 데이터가 몇 개든 상관없이 항상 하나의 데이터만 필요하다는 장점을 갖고 있어, 메모리가 많이 필요한 대용량 데이터 처리에서 효율적으로 사용할 수 있다.

또한 이터러블이기에 [...items]와 같이 배열로 바꾸어서 사용할 수도 있다.
단 이 경우, 모든 평가가 끝난 뒤에 배열이 생성되므로 사실상 제너레이터의 이점을 모두 잃어버리기 때문에 권장하지 않는다고 한다.

당장 쓸일이 있을지는 모르겠지만, 유용한 지식을 하나 배운 것 같다.

post-custom-banner

0개의 댓글