안녕하세요. 이번 시간에는 반복기와 생성기에 대해서 알아보겠습니다. 이름만 들었는데도 생소해 보이죠? 반복되는 객체를 직접 만드는 기능입니다. 어떻게 사용되는지 봅시다.
반복기
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
또 다른 점은 return과 throw를 호출할 수도 있습니다. 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 이 나오면서 해결됩니다.
다음 시간은 모듈 시스템에 대해서 알아보겠습니다.