생성하는 문법은 간단하다.
function* recordSaving() { // *(별표)를 놓지지 마라.
yield 10; // 첫번째 실행의 리턴 값이다.
yield ; // 리턴 값이니 undefined도 가능하다.
yield 20; // 세번째 실행의 리턴 값.
return 100; // 마지막 실행의 리턴 값.
}
실행하는 방법은 좀 다르다. 그냥 recordSaving() 처럼 실행하면 이것은 generator Object를 리턴한다. 리턴 된 이놈은 iterable하다. 즉, next() 메소드를 가지고 있으며 해당 메소드의 done 속성이 true가 될 때까지 반복할 수 있다. [Symbol.iterator]를 직접 정의 해 본 기억 나잖아!!!
하여튼 그래서 아래와 같이 사용할 수 있다.
...(이어서)...
const generatorObj = recordSaving();
const firstReturn = generatorObj.next(); // {value: 10, done: false}
const secondReturn = generatorObj.next(); // {value: undefined, done: false}
const thirdReturn = generatorObj.next(); // {value: 20, done: false}
const fourthReturn = generatorObj.next(); // {value: 100, done: true}
const fifthReturn = generatorObj.next(); // {value: undefined, done: true}
iteratable한 만큼 아래와 같이 for ..of 세련되게 쓸 수도 있다. 약간의 주의사항이 있다면 그냥 value만을 리턴하는 return 대신 마지막 값 까지 yield로 반환 해 주어야 하는 것이다. return으로 반환 받은 값은 iterable하지 않으니까 for ..of에서 무시된다..
function* recordSaving() {
yield 100; `
yield 200; // 마지막까지 iterable한 요소를 반환 해 주자.
}
for (const value of generator) {
console.log(value)
}
// 100
// 200
그 밖에도 iterable하기 때문에 spread syntax 등 관련 api를 사용 할 수 있다.
위에서 보았듯이 generator 객체가 iterable하다는 것은 우연이 아니다. 애초에 [Symbol.iterator]를 감안한 요소다. 지난~~번 글에서 직접 정의 해 봤던 iterator를 generator로 쉽게 다시 써 보자.
변경 전)
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
// iterator 객체를 리턴하는 메소드임을 알 수 있다.
// 즉, 'next() 메소드를 가진 객체'를 리턴하는 메소드인 것이다.
return {
current: this.from,
last: this.to,
// value와 done속성을 가진 객체를 done이 true가 될 때까지.. 하지만 무한 반복 할 수도 있다.
// 물론 for ..of 루프에서 적절한 break가 있어야하고.
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// iterator를 호출하는 spread syntax가 잘 작동한다.
console.log([...range]); // 1,2,3,4,5
변경 후)
let range = {
...
[Symbol.iterator]: function*() {
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
}
// 주석을 감안하더라도 매우 간편하게 iterator를 정의 할 수 있다.
위에서 살펴본 기본적인 사용법이나 특성 외에도 몇 가지 더 있다. 매우 특이하게도, yield가 값을 리턴 해 줄 뿐만 아니라 매개변수처럼 값을 받아가기도 한다.
에시)
function* twoWay() {
const one = yield "I am first."; // *
const two = yield "I am second."; // **
console.log(one, two); // ***
const three = yield "I am third."; // ****
}
const genTwoWay = twoWay();
genTwoWay.next(); // *라인까지 실행. 리턴값은 "I am first."
genTwoWay.next('AAA'); // **라인까지 실행. 리턴값은 알지..?
// 근데 인자 부분의 'AAA'는 어디로 갈까? (답은 아래에)
const thirdExecution = genTwoWay.next('BBB'); // 여기까지 실행하면 제너레이터 함수의 ****라인까지 실행
// 된다. 왜냐? 거기가 세번째 yeild니깐. 그렇담 당연히
// ***라인의 콘솔로그까지 실행 될 것이다. 이때 one, two
// 는 각각 'AAA'와 'BBB'가 된다.
// 즉, 한번의 실행 씩 뒤로 밀리는 것이다. 일단 한번 실행
// 돼야 인자를 저장 할 yield가 존재한다고 생각하자.
const fourthExcution = genTwoWay.next('CCC') // yield가 3개 뿐인데 네번째 next를 호출했다. 결과는
// 당연히 {value: undefined, done: true}
console.log(thirdExecution, fourthExcution) // 앞의 것은 값이 있고 뒤의 것은 없겠다.
이 외에도 끝나지 않은(반환되지 않은 yield가 아직 남은) 제너레이터를 강제로 종료(return)시키거나 에러를 뱉어내게 하는 api등이 있다. 그러나.. 과연 이런 것을 내가 갖다 쓸 것인가? 잊어버리기 전에? 지금으로서는 존재함을 맛본 정도로 족할 것 같다.. ㅠㅠ
generator의 가장 큰 특징은 순차적 실행이다. 직전 실행의 결과가 이후 실행에 상관해야 할 경우 유용 할 수 있다. 특히 직전 실행의 결과가 비동기적으로 얻을 경우엔 더욱 그렇다.
예를들면 pagenation을 순회할 때 유용할 수 있다. 페이지네이션을 구현하는 방법은 'next page'의 링크를 현재 페이지에 같이 로드 시켜주거나, 현재 페이지의 위치에 해당하는 cursor값을 같이 로드해서 다음 커서를 찾아가는 식이다. 다음페이지로 넘어가기 위해서 이전 페이지에서 어떤 값을 얻은 이후에 가능한 것이다.
또는 매우 큰 파일을 업로드하거나 다운로드 할 때에도, 파일을 조각내서 일부분 실패에 대응할 수도 있다.
generator를 활용해 쉽게 비동기적이며 순차적인 함수의 반복 실행을 간단히 구현 해 보자.
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // 비동기 실행은 asyncIterator다. for await ..of에 대응한다.
return {
current: this.from,
last: this.to,
async next() {
await new Promise(resolve => setTimeout(resolve, 1000)); // 이 부분에 예를들면 http통신 따위를 하는거다.
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) {
console.log(value); // 1,2,3,4,5 이놈들 사이사이 1000ms 씩 딱딱 기다려주게 된다.
}
})()