이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

게시글

강좌11 - EcmaScript - 2년 전 등록 / 7달 전 수정

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- 무단 전재 및 재배포 금지

댓글

3개의 댓글이 있습니다.
3달 전
글들 잘 보고있습니다.. 요기는 확실히 좀 어렵네요. async, await 보고 이해 안되면 다시 와서 봐야겠습니다..ㅎㅎ
8달 전
음 이부분은 어렵네요 여러번 봐야겠습니다
8달 전
제너레이터가 ES2015에서 가장 어렵습니다 ㅠㅠ
8달 전
그래도 제로초님 덕분에 개념들이 잡혀서 다행입니다 ^^
9달 전
맨 처음 코드의
[count, cur] = [count + 1, cur * count]; 이렇게 사용한다는건 어떤의미인가요..?
[count + 1 , cur * count]된 값을 count와 cur에 다시 저장한다는 의미인가요..?
return 에서 next()를 호출하지 않았는데도 진행되는건 내부적으로 next()가 실행되서 그런건가요??
9달 전
[count, cur] 이 부분은 객체 해체 강좌를 보시면 됩니다. 윤자님이 생각하시는 게 맞습니다. next는 for~of 문에서 내부적으로 실행됩니다.