// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorResultObject = function(idx) { return { // IteratorResult 인터페이스를 준수한 객체를 반환 value: this.slice(-idx)[0], // value 값이 반환됨. done: --idx === this.length }; };
// this를 바인딩해야하므로 ES5식 함수 사용 const makeIteratorObject = function() { let idx = 0; return { // Iterator 인터페이스를 준수한 객체를 반환 next: () => { // 이 next 함수 안에 있는 내용은 매번 실행됨. // IteratorResult 인터페이스를 준수한 객체를 반환 return makeIteratorResultObject.call(this, ++idx); } } };
const arr = [1, 2, 3, undefined, 0];
// arr은 Iterable 인터페이스를 준수한 객체가 됨. arr[Symbol.iterator] = function() { // 요 함수에 있는 내용은 한 번만 실행됨. // Iterator 인터페이스를 준수한 객체를 반환 return makeIteratorObject.call(this); };
const arr2 = [...arr]; // [0, undefined, 3, 2, 1]
IteratorResult 객체, Iterator 객체, Iterable 객체, 이 3개를 다 구현하기란 매우 귀찮고 어렵다. 따라서 제너레이터를 사용하면 아래와 같이 바꿀 수 있게 된다.
for(const a of arr) console.log(a); // 0 undefined 3 2 1
마법과도 같은 일이 일어났다. 그럼 이 마법같은 일을 낱낱이 파헤쳐보자.
Generator
일단 두 가지 케이스가 눈에 띄었다. function 키워드 뒤의 *와 yield. funcion 키워드 뒤의 *는 이 함수가 제너레이터 함수라는 것을 명시해주는 기능을 한다. 그리고 그 일반 함수가 아닌 제너레이터 함수에서는 yield 키워드를 쓸 수 있다. 이 yield는 return과 마찬가지로 값을 반환하는 기능을 하는데, 함수는 종료시키지 않는다. next를 호출할 때마다 yield 구문까지의 코드를 실행하고 yield 값을 반환하게 되는 것이다. 그리고 실행 컨텍스트를 어디선가 물고 있어서 코드의 흐름과 상관없이 next를 호출할 때마다 그 실행 결과를 보장받게 되는 것이다. 몇 가지 예제들을 통해 그 특성들을 알아보자.
// 하지만 yield 3 자체는 undefined를 반환해서 b에는 undefined가 찍힘. // 그리고 c에 yield 4가 할당되고, 4가 반환됨. console.log(a.next());
// 원래 c에는 yield 4가 할당돼 undefined가 찍혀야하지만, next에 매개변수를 주면 c에 새로운 값을 할당하게 됨. // 따라서 9를 출력. console.log(a.next(9));
// return 이후로는 닿질 못한다. console.log(a.next()); // { value: undefined, done: true } console.log('-----------------');
// 이터레이터 내부의 요소(yield)들을 모두 소모했으므로 재충전(?) a = gen(); // 이터레이터이므로 for of 구문을 사용할 수 있다. // 반환 값으로는 yield에 지정한 값들이 반환된다. for(const v of a) console.log(v);
// 제너레이터 함수의 실행 결과로 이터레이터 객체가 a에 담기게 된다. const a = gen();
// a.next()까지 하게 되면 IterableResult 객체인 { value: { obj: 2 }, done: false }가 반환된다. // 실제 for of와 같은 문법에서도 value 값을 반환하게 되는 것이다. console.log(a.next().value);
// yield에도 *(asterisk)를 찍어줄 수 있는데 yield를 쪼갠다고 보면 된다. console.log(a.next().value); // 1 console.log(a.next().value); // 2 console.log(a.next().value); // 3 console.log(a.next().value); // 4
// 그 다음엔 제너레이터 함수를 즉시 실행했으므로 이터레이터 객체가 반환된다. // 그 반환된 이터레이터 객체를 *을 써서 또 쪼갰다. console.log(a.next().value); // 5 console.log(a.next().value); // 6
// 이번엔 제너레이터 함수의 실행 결과가 아닌 함수 자체를 리턴했으므로 쪼갤 수가 없다. // 따라서 그 함수를 실행한 이터레이터 객체를 b에 따로 담아서 쪼개줘야한다. const b = a.next().value(); console.log(b.next().value); // 7 console.log(b.next().value); // 8
제너레이터 함수의 스코프를 벗어나는 공간에 yield를 쓸 수 없다.
1 2 3 4 5
const gen = function*() { const arr = [1, 2, 3]; // 콜백 함수는 제너레이터 함수가 아니므로 yield를 쓸 수 없다. arr.forEach(v =>yield v); }
제너레이터를 통해 비동기 함수를 제어하는 방법이 있지만 ES2017의 async와 await를 적극 활용하기 바란다.