게시글

강좌11 - ECMAScript - 3년 전 등록 / 2년 전 수정

ES2015(ES6) 반복기(iterator), 생성기(generator)

비동기코드를 동기처럼
조회수:
0

 안녕하세요. 이번 시간에는 반복기생성기에 대해서 알아보겠습니다. 이름만 들었는데도 생소해 보이죠? 반복되는 객체를 직접 만드는 기능입니다. 어떻게 사용되는지 봅시다.

반복기

let factorial = {
  [Symbol.iterator]() {
    let count = 1;
    let cur = 1;
    return {
      next() {
        [count, cur] = [count + 1, cur * count];
        return { done: false, value: cur };
      }
    }
  }
};
for (let n of factorial) {
  if (n > 10000000) {
    break;
  }
  console.log(n);
} // 1, 2, 6, 24, 120, 720, 5040, 40320, ...

팩토리얼 수를 나열하는 반복기를 만들었습니다. [Symbol.iterator]이 나오는 데 이건 그냥 반복되는 규칙을 내부적으로 처리하는 것을 만드는 부분이라고 생각하시면 됩니다. Symbol 자체가 자바스크립트 내부에서 일어나는 일에 접근할 수 있는 값이거든요. 그 중에서 iterator 속성은 반복을 담당합니다. Symbol에 대한 강좌는 나중에 나올 예정입니다!

Symbol.iterator의 return 안에 next가 있는데, 이 부분을 만들어주어야 제대로 된 반복기입니다. 여기서 한 번 더 return을 하는데 done반복 완료 여부를 알려주고, value현재값을 나타냅니다. factorial은 무한 수열이라서 반복 완료가 항상 false입니다.

과거에 반복기가 없을 때는 다음과 같이 만들었습니다.

var factorial2 = function() {
  var count = 1;
  var cur = 1;
  return {
    next: function() {
      cur *= count++;
      return { done: false, value: cur };
    }
  };
};
var fact = factorial2();
fact.next().value; // 1
fact.next().value; // 2
fact.next().value; // 6

직접 하나씩 next 메소드를 호출하면서 다음 수를 불러왔죠. ES2015가 지원하는 반복기는 Symbol.iterator을 통해서 내부적으로 반복이 처리되기 때문에 for - of 문 같은 것을 사용할 수 있습니다. 사실 for - of 문이 가능한 것들은 모두 Symbol.iterator 속성을 가지고 있습니다. (문자열, 배열, 객체 등)

생성기

ES2015의 반복기에는 단점이 있습니다. 위의 factorial2 함수는 fact.next().value; 처럼 직접 반복을 호출하고 다음으로 넘어가는 것을 관리할 수 있었습니다. 하지만 반복기는 그렇지 못합니다. 한 번 생성하면 그저 무한히 반복할 뿐입니다. 그래서 나온 게 생성기입니다.

function* factGenerator() {
  let cur = 1;
  let count = 1;
  while (true) {
    [count, cur] = [count + 1, cur * count]
    yield cur;
  }
}
let factGen = factGenerator();
factGen.next().value; // 1
factGen.next().value; // 2
factGen.next().value; // 6

상당히 위에 나온 factorial2 함수와 비슷하네요. 생성기에서 자동으로 { done, value }를 return한다는 것 빼고는 차이가 없어보입니다. 주의해야할 것은 새로 도입된 function*yield 구문입니다. funciton 뒤에 별이 붙었죠? function*은 이게 반복기라는 것을 알려주고, yield는 value 값을 정해줍니다.

function* customGenerator() {
  yield 1;
  yield 'zero';
  yield ['nero', 'hero'];
}
let cusGen = customGenerator();
cusGen.next(); // { value: 1, done: false }
cusGen.next(); // { value: 'zero', done: false }
cusGen.next(); // { value: ['nero', 'hero'], done: false }
cusGen.next(); // { value: undefined, done: true }

yield*을 할 수도 있습니다. 해당 값을 자동으로 쪼개 반복합니다.(Symbol.iterator가 있는 값들만 쪼갤 수 있습니다) 문자열이나 배열, 다른 반복기 값을 넣을 수 있습니다. 이게 다른 점이죠.

function* stringCutter(string) {
  yield* string;
}
const cutter = stringCutter('Zero');
cutter.next().value; // 'Z'
cutter.next().value; // 'e'

next를 통해 생성기에 값을 전달해줄 수도 있습니다. 전달해준 값은 yield가 받습니다. yield가 받은 값과 next로 나오는 값은 별개입니다. 또 첫 번째 값 전달은 무시된다는 것을 기억하세요!

function* deliverGenerator() {
  console.log('generator called');
  let val = yield;
  console.log('1st', val);
  val = yield 2;
  console.log('2nd',val);
  val = yield;
  console.log('3rd', val);
}
let delGen = deliverGenerator();
delGen.next(1); // 'generator called'
delGen.next(2); // '1st 2', next의 value는 2 
delGen.next(3); // '2nd 3'
delGen.next(4); // '3rd 4', next의 done은 true

또 다른 점은 returnthrow를 호출할 수도 있습니다. cutter.return();하는 순간 생성기는 멈추고, done은 true가 됩니다. 반복을 멈추고 싶을 때 사용하면 됩니다. cutter.throw();는 return처럼 생성기를 종료하고, 추가로 에러를 만들어냅니다. 따라서 try catch 구문에서 catch로 보낼 수 있습니다.

지금까지 생성기에 대해 알아봤는데요. 여러분들 중 일부는 유용하다고 느낄 것이고, 일부는 이걸 어디에 써? 이런 느낌을 갖고 계실 겁니다. 하지만 이 녀석은 매우 무시무시한 녀석입니다. 아까 while문 안에 yield를 넣으면 while문이 next하기 전까지 멈춰있었죠? yield가 코드의 흐름을 끊고 있습니다. 이것을 활용해서 비동기 코드를 동기로 눈속임하는 짓을 합니다...

function async(gen) {
  let iterator = gen();
  let result;
  (function iterate(val) {
    result = iterator.next(val); // yield에 값을 전달
    if (!result.done) { // iterator의 끝까지 반복
      if (result.value.then) { // result의 value가 promise 객체이면
        result.value.then(iterate); // promise의 then을 실행
      } else { // promise 객체가 아니면
        setTimeout(() => { // 비동기로
          iterate(result.value); // 다음 value를 yield로 보내서 처리
        }, 0);
      }
    }
  })();
}

위와 같은 async 함수를 만들어줍니다. 이제 이 함수를 사용해서 비동기 코드를 동기로 흉내낼 수 있습니다. Promise 시간 의 Mongoose 코드를 다음과 같이 할 수 있는 거죠. Promise 시간의 코드와 같이 띄워놓고 비교해보세요.

async(function* findUser() {
  try {
    let user = yield Users.findOne({}).exec();
    user.name = 'zero';
    user = yield user.save();
    user = yield Users.findOne({ gender: 'm' }).exec();
    ...
  } catch(err) {
    console.error(err);
  }
});

완전 동기 코드같죠? promise의 catch 처리는 생성기에서는 try catch 문으로 해주면 됩니다. 단점은 async 함수를 매번 구현해줘야 한다는 건데요. 나중에 ES2017에서 async/await 이 나오면서 해결됩니다.

다음 시간은 모듈 시스템에 대해서 알아보겠습니다.

투표로 게시글에 관해 피드백을 해주시면 많은 도움이 됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright © 2016- 무단 전재 및 재배포 금지

댓글

4개의 댓글이 있습니다.
3달 전
처음에 [Symbol.iterator](){ ... } 이 부분이 생소합니다. 메소드에 array notation을 쓰는 이유가 무엇인가요? 좋은 튜토리얼 감사합니다
3달 전
es6 객체에 추가된 동적 속성명입니다. symbol.iterator는 약속입니다
일 년 전
글들 잘 보고있습니다.. 요기는 확실히 좀 어렵네요. async, await 보고 이해 안되면 다시 와서 봐야겠습니다..ㅎㅎ
2년 전
음 이부분은 어렵네요 여러번 봐야겠습니다
2년 전
제너레이터가 ES2015에서 가장 어렵습니다 ㅠㅠ
2년 전
그래도 제로초님 덕분에 개념들이 잡혀서 다행입니다 ^^
2년 전
맨 처음 코드의
[count, cur] = [count + 1, cur * count]; 이렇게 사용한다는건 어떤의미인가요..?
[count + 1 , cur * count]된 값을 count와 cur에 다시 저장한다는 의미인가요..?
return 에서 next()를 호출하지 않았는데도 진행되는건 내부적으로 next()가 실행되서 그런건가요??
2년 전
[count, cur] 이 부분은 객체 해체 강좌를 보시면 됩니다. 윤자님이 생각하시는 게 맞습니다. next는 for~of 문에서 내부적으로 실행됩니다.