코드스테이츠 부트캠프 프론트엔드 44기
릴레이 블로깅 챌린지 2주차 (월요일)


제너레이터(Generator)란?

function* genFunc() {  // 제너레이터 함수 생성
  yield 1;
  yield 2;
  yield 3;
}

const generator = genFunc();  // 제너레이터 객체 반환

console.log(generator.next());  // {value: 1, done: false}
console.log(generator.next());  // {value: 2, done: false}
console.log(generator.next());  // {value: 3, done: false}
console.log(generator.next());  // { value: undefined, done: true }
console.log(generator.next());  // { value: undefined, done: true }

제너레이터(Generator)란 yield문을 사용한 이터레이터를 말한다. Python과 C#에도 똑같은 개념이 존재한다.

제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아닌, 제너레이터 객체를 생성 & 반환한다. 클래스와 인스턴스의 관계랑 비슷하다고 생각하면 쉬울 것이다.

제너레이터는 이터레이터와 달리 Symbol.iterator 메서드를 일일이 호출하지 않아도 된다는 장점이 있다.


특징

1. 가독성 향상

/* 이터레이터 */

const fibonacci = {
  [Symbol.iterator]() {
    let [pre, cur] = [0, 1];
    return {
      next() {
        [pre, cur] = [cur, pre + cur];
        return { value: cur, done: false };
      }
    };
  }
};
const iter = fibonacci[Symbol.iterator]();
/* 제너레이터 */

function* fibonacci() {
  let [pre, cur] = [0, 1];
  while (true) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
};
const generator = fibonacci();

1초에 한 번씩 피보나치 수를 반환 & 출력하는 코드를 이터레이터(上)와 제너레이터(下)로 각각 작성해보았다.

이터레이터가 불편한 이유

이터레이터의 경우 [Symbol.iterator]() { }의 존재와 return문 중첩 때문에 코드 블럭이 무려 4중으로 이루어져 있다. 게다가 .next() 메서드의 return 양식({ value: foo, done: bar })까지 프로그래머가 수동으로 지정해야 한다.

게다가 10개만 출력하는 경우처럼 '유한한 이터레이터'를 만들고 싶을 경우 조건문이 필요한데, 이때 가독성이 더더욱 떨어진다.

/* 이터레이터 : 10개만 추출 */

const fibonacci = {
  [Symbol.iterator]() {
    let i = 0;  // 현재 인덱스
    let [pre, cur] = [0, 1];
    return {
      next() {
        [pre, cur] = [cur, pre + cur];
        return (++i > 10) ? { value: undefined, done: true } : { value: cur, done: false };
      }
    };
  }
};
const iter = fibonacci[Symbol.iterator]();


for (const result of fibonacci) {
  console.log(result);  // 1 2 3 5 8 13 21 34 55 89
}

인덱스를 담는 변수(i)를 추가한 탓에 안 그래도 난잡했던 코드가 더더욱 난잡해졌다. 게다가 조건문에 따라 return 양식을 다르게 지정해줘야 한다.

이런 점들 때문에 '사용자 지정 이터레이터'는 가독성이 상당히 나쁘다.

제너레이터가 편한 이유

반면 제너레이터의 경우 코드 짜기가 상당히 편하다. 10개만 출력되도록 만들고 싶을 경우 그냥 while문을 for문으로 바꾸면 끝난다.

/* 제너레이터 */

function* fibonacci() {
  let [pre, cur] = [0, 1];
  for (let i = 0; i < 10; i++) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
};
const generator = fibonacci();

for (const result of generator) {
  console.log(result);  // 1 2 3 5 8 13 21 34 55 89
}

보다시피 [Symbol.iterator]() { }를 작성하지 않아도 되고, return 역할을 하는 yield 키워드 하나만 작성하면 된다. 무엇보다도 .next() 메서드 양식을 사용자가 지정하지 않아도 된다. 제너레이터가 다 알아서 해주기 때문이다.

이런 점에서 이터레이터의 가독성을 개선하고 싶을 경우 제너레이터가 대안이 될 수 있다.

2. 인수 전달 가능 ★

function* genFunc() {
  const x = yield 'a';
  const y = yield 'b';
  const z = yield 'c';

  console.log(x);  // 1
  console.log(y);  // 2
  console.log(z);  // 3
}
const generator = genFunc(0);


let res = generator.next();
console.log(res);  // { value: 'a', done: false }

res = generator.next(1);
console.log(res);  // { value: 'b', done: false }

res = generator.next(2);
console.log(res);  // { value: 'c', done: false }

res = generator.next(3);
console.log(res);  // { value: undefined, done: true }

yield 키워드는 반환(return), 코드 일시정지, 인자 수령이라는 3가지 역할을 모두 담당한다.


이터레이터 → 제너레이터 변환

1. 배열을 사용하기 불편한 경우

const arr = [1, 2, 3];

function* genFunc() {
  for (const item of arr) {
    yield item;
  }
}
const generator = genFunc();

function isOdd() {
  const num = generator.next().value;
  return num % 2 === 1;
}

function sumTwoNums() {
  const x = generator.next().value;
  const y = generator.next().value;
  return x + y;
}

console.log(isOdd(), sumTwoNums());  // true, 5
console.log(arr);  // [ 1, 2, 3 ]

console.log(generator.next());  // { value: undefined, done: true }

2. 데이터가 무한일 경우

const fibonacci = (function* () {
  let [pre, cur] = [0, 1];

  while (true) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
}());

setInterval(() => console.log(fibonacci.next().value), 1000);

기존 이터레이터에 비해서 코드가 상당히 간단해졌고, 직관적으로 바뀐 것을 알 수 있다.


3. 최적화

const boolIter = (function* () {
  for (let i = 0; i < 100_000_000; i++) {
    yield true;
  }
}());

for (const bool of boolIter) { }

이 코드의 실행 속도는 약 1.2초~1.4초이다.

느리다!

[전체 코드 실행 시간]
배열(2.3~2.7초) > 제너레이터(1.2~1.4초) > 이터레이터(0.1초)

[순회 코드 실행 시간]
제너레이터(1.2~1.4초) > 배열(0.6~1.2초) > 이터레이터(0.1초)

예상 외로 제너레이터 속도가 빠르지 않았다. 오히려 for문 순회만 놓고 봤을 때 제너레이터가 제일 느렸다. 왜 그런 걸까?

추측하건대 yield 자체가 좀 느린 것 같다. 배열/이터레이터는 return을 사용하는데, return은 말 그대로 반환만 시켜주면 그걸로 역할이 끝난다. 반면 yield는 반환(return), 코드 일시정지, 인자 수령이라는 3가지 역할을 모두 담당해야 한다. 이러한 yield 특유의 성질로 인해 return문에 비해 속도가 느려진 것으로 추정된다.


결론

  • '실행 속도 최적화'가 목적이라면? → 이터레이터
  • '가독성 & 유지보수성 향상'이 목적이라면? → 제너레이터


<주의 사항>

이 게시물은 코드스테이츠의 블로깅 과제로 제작되었습니다.
때문에 설명이 온전치 못하거나 글의 완성도가 낮을 수 있습니다.

profile
Self-improvement Guarantees Future.

0개의 댓글