
1. 제너레이터
* 제너레이터란
- 일반적으로 함수는 0개 또는 하나의 값을 반환한다. 하지만 제너레이터(generator)는 반환값을 여러개로 만들 수 있다.
- 제너레이터는 일반적인 함수가 아닌 제너레이터 함수를 통해 만들게 된다.
* 제너레이터 만들기
- 제너레이터는 '*' 키워드와 함께 만들 수 있다.
function* test() {
yield 1;
yield 2;
return 3;
}
const generator = test();
const one = generator.next();
const two = generator.next();
const three = generator.next();
console.log(one); // { value: 1, done: false }
console.log(two); // { value: 2, done: false }
console.log(three); // { value: 3, done: true }
- 위 코드에서 보이는 일반함수와 가장 큰 차이점은 함수 호출시 코드가 실행되지 않는다는 점이다. 제너레이터 함수는 호출되면 제너레이터 객체가 반환된다.
- next는 제너레이터의 핵심 메서드로, 호출될 때 yield <value> 문을 만날 때 까지 실행이 지속된다. yield 문을 만나면 실행이 멈추고 value가 반환된다. 이 때 next는 value 뿐아니라 함수 코드 실행의 종료를 알리는 done 프로퍼티도 함께 담아서 객체를 반환한다.
2. 이터러블과 제너레이터
* 이터레이터
- 반복문은 이터레이터(iterator, next 가 있는 객체)를 대상으로 동작하며, 다음 값이 필요할 때마다 value와 done 프로퍼티를 가진 객체를 반환하는 next() 메서드를 호출한다. 따라서 next 가 존재하는 제너레이터는 이터러블(반복가능한)객체이다.
const range = {
from: 1,
to: 10
};
- 제너레이터가 이터러블임을 확실히 하기위해 이터레이터를 통해 이터러블 객체를 만들어보자. 위의 객체를 반복가능하도록 만드는 코드는 아래와 같다.
range[Symbol.iterator] = function() {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
- 반복문(for...of)가 호출될 때, Symbol.iterator가 호출된다. 이는 이터레이터 객체를 반환하게되고, 이후부터 for...of는 반환된 이터레이터 객체만을 대상으로 동작한다.
- 이제 반복마다 next() 가 호출되는데, 이는 값을 반드시 done과 value가 갖춰진 객체로 만들어 반환해야한다. 그래서 위의 코드는 조건을 확인하여 이 객체를 반환하도록 했다.
- 범위가 to 에 도달했을 때, done true를 통해 끝나도록 하였다.
for (let num of range) {
console.log(num); // 1, 2, 3, ... , 10
}
- 반복문에서 원하는 대로 값을 얻는 것을 확인할 수 있다.
- 이터레이터 객체와 반복 대상 객체를 합쳐서 range 자체를 이터레이터로 만들면 코드가 더 간결해진다.
const range = {
from: 1,
to: 10,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
- 코드를 보면 알겠지만 제너레이터와 비슷한 점이 많다. next 메서드가 있으며 반환되는 객체가 done과 value 프로퍼티를 갖고있다. 즉 제너레이터는 이터러블이다.
* 제너레이터
- 제너레이터가 내부적으로 next가 있음을 알고 있고, 위에서 next가 있으면 이터러블 이라고 했으니 반복문을 통해 알아보자.
function* test() {
yield 1;
yield 2;
return 3;
}
const generator = test();
const generator2 = test();
for (let value of generator) {
console.log(value); // 1, 2
}
console.log([...generator2]) // [1, 2]
- next().value를 호출하는 것보다 더 깔끔하게 value 만을 출력하게 하였다. 보다시피 반복문이 잘 돌고 있다. 그러나 한가지 이상한 점이 있다. 바로 3이 출력되지 않은것이다.
- 위에 이터레이터 예제에서 봤듯이 done:true일 경우는 반복문에서 무시된다. 3을 출력하려면 yield 3; 을 추가해야한다.
- 이터레이터 예제에서 봤던 range를 제너레이터 함수로 구현하면 다음과 같다.
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
- 반환 값이 자동으로 {value: ??, done: ??} 형태이므로 코드가 훨씬 짧아졌음을 볼 수 있다.
3. 제너레이터의 기능
* 제너레이터 컴포지션
- 제너레이터 컴포지션(generator composition)을 제너레이터 안에 제너레이터를 임베딩하는 기능이다.
- 숫자 0부터 9까지 (문자코드 48~57), 알파벳 대문자 A부터 Z까지(문자 코드 65~90)을 연속으로 가지는 비밀번호를 만든다고 가정하자. 위 두가지 로직이 별개의 제너레이터라고 가정하자. 그렇다면 여러개의 함수를 만들고, 그 결과를 조합해야한다. 하지만 제너레이터의 yield* 키워드를 사용하면 제너레이터를 다른 제너레이터에 넣을 수 있다.
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
}
let str = '';
for (let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
console.log(str); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
- yield* 는 실행자체를 다른 제너레이터에 위임하는 기능을 갖고 있다. 이를 통해 마치 바깥의 제너레이터가 값을 생성한것처럼 동작하는 것이다.
* 양방향 교환
- 지금까지의 제너레이터는 바깥에 값을 전달하기만 했다. 그러나 제너레이터는 값을 안으로 전달하기도 한다.
- 값을 안으로 전달할때에는 next(arg)를 호출한다. 인수인 arg는 yield의 결과 그 자체가 된다.
function* gen() {
const q1 = yield '2 + 2 = ?';
console.log(q1);
const q2 = yield '3 + 3 = ?';
console.log(q2);
}
const generator = gen();
console.log(generator.next().value);
console.log(generator.next(4).value);
console.log(generator.next(9).done);
// 2 + 2 = ?
// 4
// 3 + 3 = ?
// 9
// true
- next를 처음 호출할 땐 인수가 필요없다. 첫 yield의 결과가 반환되어 question에 담긴다. 이 때 제너레이터는 yield 줄에 멈춘다.
- 다음으로 next(4)를 호출하였다. 이 때, q1에 4가 담기게된다. 그리고 다음 yield까지 실행되므로 console.log(q1)이 실행되고 두 번째 yield 의 결과가 반환된 후 멈춘다.
- 위 과정이 반복되어 마지막에는 yield가 더 이상 없으므로 done 이 true로 반환된다.
- 모던 자바스크립트에서는 제너레이터를 잘 사용하지 않지만 제너레이터 호출 코드와 데이터를 교환할 수 있기 때문에 유용하고, 이터러블 객체를 쉽게 만들 수 있다는 장점이 있다. 그리고 비동기 제너레이터를 활용하여 데이터 스트림을 다룰 때에도 유용하다.
참고
제너레이터
ko.javascript.info
iterable 객체
ko.javascript.info

'Language > JavaScript' 카테고리의 다른 글
<ES2021> ES2021 Features (0) | 2021.10.25 |
---|---|
<ES2020> ES2020 Features (0) | 2021.10.24 |
<ES10> ES2019 Features (0) | 2021.10.11 |
<ES9> ES2018 Features (0) | 2021.10.10 |
<이벤트루프> 태스크 큐 : 마이크로태스크 큐 & 매크로태스크 큐 (0) | 2021.10.04 |