ECMAScript 6 Iterator와 Generator

Iterator와 Generator

많은 프로그래밍 언어는 컬렉션에서 위치를 추적하기 위해 변수가 필요한 for 루프를 사용하여 데이터를 반복하는 것에서 컬렉션의 다음 항목을 반환하는 Interator 객체를 사용하는 방식으로 전환했습니다. Iterator를 사용하면 데이터 컬렉션을 쉽게 처리할 수 있어 ECMAScript 6에서는 Iterator를 JavaScript에 추가했습니다. Iterator는 새로운 Array 메서드 및 새로운 타입의 컬렉션 (SetMap)과 결합하여 데이터를 효율적으로 처리할수 있는 핵심 요소이며, 이러한 부분은 JavaScript의 여러곳에서 찾아볼 수 있습니다. 또한 Iterator와 함께 작동하는 새로운 for-of 루프가 있으며, Spread (...) 연산자에서도 Iterator를 사용할 수 있습니다. 그리고
Iterator는 비동기 프로그래밍을 더 쉽게 만들수 있게 합니다.

이 장에서는 Iterator의 많은 용도에 대해 다루지만, 먼저 Iterator가 JavaScript에 추가된 이유에 대한 역사를 이해하는 것이 중요합니다.

루프(Loop) 문제점

JavaScript로 프로그래밍한 적이 있다면 아마도 다음 코드가 익숙할 것입니다.

1
2
3
4
5
var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}

이 표준 for 루프는 인덱스를 colors Array에 대해 i 변수로 추적합니다. i의 값은 i가 (len에 저장된) Array의 길이보다 크지 않다면 루프가 실행될 때마다 증가합니다.

이 예제의 루프는 매우 간단하지만 루프를 중첩하여 여러 변수를 추적해야 할 때 매우 복잡해집니다. 추가적인 복잡성으로 인해 오류가 발생할 수 있으며, for 루프의 상용구는 유사한 코드가 여러 위치에 작성되어 더 많은 오류를 발생시킬 수 있습니다. Iterator는 이 문제를 해결하기 위한 것입니다.

Iterator는 무엇일까요?

Iterator는 반복을 위해 설계된 특정 인터페이스가 있는 객체입니다. 모든 Iterator 객체는 결과 객체를 반환하는 next() 메서드를 가지고 있습니다. 결과 객체에는 두 가지 프로퍼티, 즉 다음 값인 value와 반환할 값이 더 이상 없을 때 true 인 부울 값인 done 프로퍼티입니다. Iterator는 값 컬렉션 내의 위치에 대한 내부 포인터를 유지하고 next() 메서드를 호출할 때마다 다음 적절한 값을 반환합니다.

마지막 값이 반환된 후에 next()를 호출하면 메서드는 donetrue로 리턴하고 valueIterator리턴 값을 포함합니다. 이 리턴 값은 데이터의 일부가 아니며 관련 데이터의 마지막 부분이거나 그러한 데이터가 없으면 undefined입니다. Iterator의 리턴 값은 정보를 호출자에게 전달하는 마지막 방법이라는 점에서 함수의 리턴 값과 유사합니다.

이를 염두에 두고 ECMAScript 5에서 Iterator를 만드는 것은 아래와 같이 간단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"

createIterator() 함수는 next() 메서드를 가진 객체를 반환합니다. 메서드가 호출될 때마다 items Array의 다음 값은 value로 리턴됩니다. i가 3 일 때 donetrue가 되고 value를 설정하는 삼항 조건 연산자는 undefined로 평가됩니다. 이 결과는 ECMAScript 6에서는 마지막 데이터가 사용된 후 next()가 호출될 때와 같은 특수한 역할을 합니다.

이 예제에서 보듯이, ECMAScript 6에 규정된 규칙에 따라 동작하는 Iterator 작성은 약간 복잡합니다.

다행히도 ECMAScript 6은 Iterator 생성자를 제공하여 Iterator 객체를 훨씬 쉽게 만들 수 있습니다.

Generator는 무엇일까요?

GeneratorIterator를 반환하는 함수입니다. Generator 함수는 function 키워드 다음에 별표 (*)가 추가되고 새로운 yield 키워드를 사용합니다. 별표가 function 바로 앞에 있는지 또는 *function 사이에 공백이 있는지는 중요하지 않습니다. 아래 예제를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// generator
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
// generator는 일반 함수처럼 호출되지만 iterator를 반환합니다.
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

createIterator() 이전의 *는 함수를 Generator로 만듭니다. ECMAScript 6에 새로 도입된 yield 키워드는 next()가 호출될 때 결과 Iterator가 리턴 해야하는 값을 리턴될 순서대로 지정합니다. 이 예제에서 생성된 Iteratornext() 메서드를 연속적으로 호출할 때 세가지 다른 값을 리턴합니다. : 1, 2 그리고 마지막으로 3. Generatoriterator를 생성할 때 본 것처럼 다른 함수와 똑같이 호출할 수 있습니다.

아마도 Generator 함수 중 가장 흥미로운 부분은 각 yield 문 다음에 실행을 멈추는 것입니다. 예를 들어, 이 코드에서 yield 1을 실행 한 후에, 함수는 Iteratornext() 메서드가 호출될 때까지 다른 것을 실행하지 않고, next()가 호출 되는 시점에 yield 2가 실행됩니다. 함수 중간에 실행을 중지하는 이 기능은 매우 강력하며 Generator 함수의 사용을 흥미롭게 합니다. (“Iterator의 고급기능”섹션에서 논의 함).

yield 키워드는 모든 값이나 표현식과 함께 사용할 수 있으므로 항목을 하나씩 나열하지 않고 Iterator에 항목을 추가하는 Generator 함수를 작성할 수 있습니다. 예를 들어, for 루프에서 yield를 사용할 수있는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 예제는 items라는 ArraycreateIterator() Generator 함수에 전달합니다. 함수 내에서 for 루프가 진행됨에 따라 Array에서 Iterator 요소를 생성합니다. yield가 발생할 때마다 루프가 멈추고 iteratornext()가 호출될 때마다 루프의 다음 yield 문이 실행 됩니다.

Generator는 ECMAScript 6의 중요한 기능이며, 함수이기 때문에 모든 곳에서 사용할 수 있습니다. 이 섹션의 나머지 부분에서는 Generator를 작성하는 다른 유용한 방법에 중점을 둡니다.

yield 키워드는 Generator 내부에서만 사용할 수 있습니다. 예를 들면 다음과 같이 Generator 내부 함수에서 사용하는 것을 포함하여 다른 곳에서 yield를 사용하는 것은 구문 오류입니다.

1
2
3
4
5
6
7
8
function *createIterator(items) {
items.forEach(function(item) {
// syntax error
yield item + 1;
});
}

yield는 기술적으로 createIterator() 내부에 있지만, yield는 함수의 경계를 넘을 수 없으므로 이 코드는 구문 오류입니다. yieldreturn과 비슷한 방식으로 사용되었지만 중첩된 함수는 그 함수를 포함하는 값을 리턴할 수 없습니다.

Generator 함수 표현식

함수 표현식을 사용하여 function 키워드와 여는 괄호 사이에 별표 (*) 문자를 포함시킴으로써 Generator를 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let createIterator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 코드에서, createIterator()는 함수 선언 대신에 Generator 함수 표현식입니다. 함수 표현식이 익명이므로 별표는 function 키워드와 여는 괄호 사이에 옵니다. 그 외에 이 예제는 for 루프를 사용했던 createIterator() 함수의 이전 버전과 동일합니다.

Arrow 함수를 이용해 Generator를 만들수 없습니다.

Generator 객체 메서드

Generator는 함수이기 때문에 객체에도 추가할 수 있습니다. 예를 들어, 함수 표현식을 사용하여 ECMAScript 5 스타일 객체 리터럴에서 Generator를 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
var o = {
createIterator: function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);

메서드 이름 앞에 별표(*)를 붙임으로써 ECMAScript 6 단축 메서드를 바로 사용할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
var o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);

이 예제는 “Generator 함수 표현식” 섹션의 예제와 기능적으로 동일합니다. 단지 다른 구문을 사용했을 뿐입니다. 단축 버전에서는
createIterator() 메서드가 function 키워드 없이 정의되었고 별표와 메서드 이름 사이에 공백을 둘 수 있지만 별표는 메서드 이름 바로 앞에 위치해야 합니다.

Iterables 과 for-of

Iterator와 밀접하게 관련된 IterableSymbol.iterator 프로퍼티를 가진 객체입니다. 잘 알려진 Symbol.iterator Symbol은 주어진 객체에 대한 Iterator를 반환하는 함수를 지정합니다. ECMAScript 6에서는 모든 컬렉션 객체 (Array, SetMap) 및 문자열이 Iterable이므로 기본 Iterator가 지정되어 있습니다. Iterable은 ECMAScript에 새로 추가된 for-of 루프와 함께 사용하도록 설계되었습니다.

Generator가 기본적으로 Symbol.iterator 프로퍼티를 할당하므로 Generator가 만든 모든 IteratorIterable입니다.

이 장의 시작 부분에서, for 루프 내에서 인덱스를 추적하는 문제에 대해 언급했습니다. 이 문제를 해결하는 첫 번째는 Iterator입니다. 그리고 두 번째는 for-of 루프입니다. 컬렉션에 대한 인덱스를 추적할 필요가 없기 때문에 컬렉션의 내용에 대한 작업에 집중할 수 있습니다.

for-of 루프는 루프가 실행될 때마다 Iterable에서 next()를 호출하고 결과 객체의 value를 변수에 저장합니다. 루프는 반환된 객체의 done 프로퍼티 값이 true가 될 때까지 이 과정을 계속합니다. 다음은 그 예제를 살펴 보겠습니다.

1
2
3
4
5
let values = [1, 2, 3];
for (let num of values) {
console.log(num);
}

이 코드는 다음을 출력합니다.

1
2
3
1
2
3

for-of 루프는 values ArraySymbol.iterator 메서드를 먼저 호출하여 Iterator를 검색합니다. (Symbol.iterator에 대한 호출은 JavaScript 엔진 자체에서 발생합니다.) 그러면 iterator.next()가 호출되고 Iterator의 결과 객체에있는 value 프로퍼티가
num으로 읽혀집니다. num 변수는 1, 2, 그리고 마지막으로 3입니다. 결과 객체에서 donetrue 일 때, 루프는 끝나기 때문에 num은 결코 undefined의 값으로 지정되지 않습니다.

단순히 Array이나 컬렉션의 값을 반복한다면 for 루프 대신에 for-of 루프를 사용하는 것이 좋습니다. for-of 루프는 일반적으로 추적하기 위한 조건이 적기 때문에 에러 발생이 전통적인 for 루프보다 적습니다. 보다 복잡한 제어 조건이 필요한 경우 전통적인for 루프를 사용 하십시오.

for-of 문은 Non-iterable 객체, null 또는 undefined에서 사용될 때 에러를 던집니다.

Default Iterator 액세스 하기

Symbol.iterator를 사용하여 다음과 같이 객체의 Default Iterator를 액세스할 수 있습니다.

1
2
3
4
5
6
7
let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 코드는 values에 대한 Default Iterator를 가져오고 이를 사용하여 Array의 각항목을 반복합니다. 이것은 for-of 루프를 사용할 때 배후에서 일어나는 것과 같은 방식입니다.

Symbol.iterator는 Default Iterator를 지정하므로 이를 사용하여 객체가 다음과 같이 반복 가능한지 여부를 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
function isIterable(object) {
return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

isIterable() 함수는 객체에 Default Iterator가 존재 하는지를 확인하는 함수입니다. for-of 루프는 실행 전에 비슷한 검사를 수행합니다.

지금까지 이 섹션의 예제에서는 내장 Iterable이 있는 Symbol.iterator를 사용하는 방법을 알아 봤지만 Symbol.iterator 프로퍼티를 사용하여 고유한 Iterable을 만들수도 있습니다.

Iterable 만들기

개발자가 정의한 객체는 기본적으로 반복 가능하지 않지만 Generator가 포함된 Symbol.iterator 프로퍼티를 만들어 반복 가능하게 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}

위 코드는 다음을 출력합니다.

1
2
3
1
2
3

먼저, 이 예제는 collection이라는 객체의 Default Iterator를 정의합니다. Default IteratorGeneratorSymbol.iterator 메서드에 의해 생성됩니다 (별표시는 여전히 이름 앞에 옵니다). Generatorfor-of 루프를 사용하여 this.items의 값을 반복하고 yield를 사용하여 각각을 리턴합니다. collection 객체는 수동으로 반복하여 collectionDefault Iterator에 대한 값을 정의하는 대신, this.itemsDefault Iterator를 사용하여 작업을 수행합니다.

이 장의 뒷부분에 나오는 “Generator 위임”에서는 다른 객체의 Iterator를 사용하는 방법에 대해 설명합니다.

이제까지 ArrayDefault Iterator에 대한 사용법을 살펴 보았지만 ECMAScript 6에는 더 많은 Iterator가 내장되어 있어 데이터 수집 작업을 쉽게 수행할 수 있습니다.

내장 Iterator

Iterator는 ECMAScript 6의 중요한 부분이므로 많은 빌트인 타입에 대해 자체적으로 Iterator를 만들 필요가 없습니다. 언어 레벨에서 기본적으로 포함하고 있습니다. 기본 제공되는 Iterator가 목적에 부합하지 않을 때만 Iterator를 만들어야 하며, 이는 자신만의 객체 나 클래스를 정의할 때 자주 발생합니다. 그 외에는 내장된 Iterator를 사용하여 작업을 수행할 수 있습니다. 아마도 가장 일반적인 Iterator는 컬렉션에서 작동하는 Iterator일 것입니다.

컬렉션 Iterator

ECMAScript 6에는 Array, MapSet 세가지 타입의 컬렉션 객체가 있습니다. 세가지 모두에는 내용을 탐색하는데 도움이되는 다음과 같은 기본 제공 Iterator가 있습니다.

  • entries() - 값이 키 - 값 쌍인 Iterator를 반환합니다.
  • values() - 값이 컬렉션의 value인 Iterator를 반환합니다.
  • keys() - 값이 컬렉션에 포함된 key인 Iterator를 반환합니다.

이 메서드 중 하나를 호출하여 컬렉션을 검색할 수 있습니다.

entries() Iterator

entries() Iteratornext()가 호출될 때마다 두개의 아이템 Array을 반환합니다. 두개의 아이템 Array는 컬렉션의 각 항목에 대한 키와 값을 나타냅니다. Array의 경우 첫 번째 항목은 숫자 인덱스입니다. Set의 경우 첫 번째 항목은 값이기도합니다 (값은 Set의 키와 동일하므로). Map의 경우 첫 번째 항목은 키입니다.

다음은이 Iterator를 사용하는 몇 가지 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
for (let entry of colors.entries()) {
console.log(entry);
}
for (let entry of tracking.entries()) {
console.log(entry);
}
for (let entry of data.entries()) {
console.log(entry);
}

console.log() 호출은 다음과 같은 결과를냅니다.

1
2
3
4
5
6
7
8
[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

위 예제는 각 컬렉션 타입에 대해 entries() 메서드를 사용하여 Iterator를 검색하고 for-of 루프를 사용하여 반복합니다. console 출력은 각 객체에 대해 어떻게 키와 값이 쌍으로 리턴되는지 보여줍니다.

values() Iterator

values() Iterator는 컬렉션에 저장된 값을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
for (let value of colors.values()) {
console.log(value);
}
for (let value of tracking.values()) {
console.log(value);
}
for (let value of data.values()) {
console.log(value);
}

이 코드는 다음을 출력합니다.

1
2
3
4
5
6
7
8
"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"

이 예제에서와 같이 values() Iterator를 호출하면 컬렉션의 해당 데이터 위치에 대한 정보없이 각 컬렉션에 포함된 정확한 데이터가 반환됩니다.

keys() Iterator

keys() Iterator는 컬렉션에 있는 각 키를 반환합니다. Array의 경우 숫자 키만 반환하며 Array의 다른 프로퍼티는 반환하지 않습니다. Set의 경우 키는 값과 동일하므로 keys()values()는 동일한 Iterator를 반환합니다. Map의 경우, keys() Iterator는 각 고유 키를 리턴합니다. 다음은 이 세가지를 모두 보여주는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
for (let key of colors.keys()) {
console.log(key);
}
for (let key of tracking.keys()) {
console.log(key);
}
for (let key of data.keys()) {
console.log(key);
}

이 예제는 다음을 출력합니다.

1
2
3
4
5
6
7
8
0
1
2
1234
5678
9012
"title"
"format"

keys() Iteratorcolors, trackingdata에서 각 키를 가져 오며, 이 키들은 세개의 for-of 루프 내부에서 출력됩니다. Array의 경우 숫자 색인만 출력되며 Array에 명명된(named) 프로퍼티를 추가 한 경우에도 숫자 색인만 출력합니다. 이것은 for-in 루프는 숫자 인덱스가 아닌 프로퍼티를 반복하기 때문에 for-in에서 Array를 이용하는 방식과 다릅니다.

컬렉션 타입에 대한 Default Iterator

각 컬렉션 타입에는 Iterator가 명시적으로 지정되지 않은 경우 for-of에 의해 사용되는 Default Iterator가 있습니다. values() 메서드는 ArraySetDefault Iterator이며, entries() 메서드는 MapDefault Iterator입니다. 이러한 기본값은 for-of 루프에서 컬렉션 객체를 사용하는 것을 좀더 쉽게 만듭니다. 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");
// colors.values()를 사용하는 것과 같습니다.
for (let value of colors) {
console.log(value);
}
// tracking.values()를 사용하는 것과 같습니다.
for (let num of tracking) {
console.log(num);
}
// data.entries()를 사용하는 것과 같습니다.
for (let entry of data) {
console.log(entry);
}

Iterator가 지정되지 않았기 때문에 Default Iterator가 사용됩니다. Array, SetMapDefault Iterator는 이러한 객체가 초기화되는 방식을 반영하도록 설계되었으므로 이 코드는 다음을 출력합니다.

1
2
3
4
5
6
7
8
"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]

ArraySet은 기본적으로 값을 반환하고, MapMap 생성자에 전달할 수 있는 것과 동일한 Array 형태를 반환합니다. 반대로 Weak SetWeak Map에는 Built-in Iterator가 없습니다. 약한 참조를 관리한다는 것은 이러한 컬렉션에 정확히 얼마나 많은 값이 있는지를 알 수있는 방법이 없다는 것을 의미합니다. 또한 이들을 반복할 방법이 없다는 것을 의미합니다.

Destructuring과 for-of 루프

Map에 대한 Default Iterator의 동작은 다음 예와 같이 Destructuring이 있는 for-of 루프에서 사용될 때도 유용합니다.

1
2
3
4
5
6
7
8
9
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
// data.entries()을 사용하는 것과 같습니다.
for (let [key, value] of data) {
console.log(key + "=" + value);
}

이 코드의 for-of 루프는 Destructured Array을 사용하여 Map의 각 항목에 대해 keyvalue를 지정합니다. 이 방법으로 두 항목 Array에 액세스하거나 Map에서 키 또는 값을 가져 오지 않고도 키와 값을 사용하여 쉽게 작업할 수 있습니다. Map에 대해 Destructured Array를 사용하여 for-of 루프가 SetArray의 경우와 마찬가지로 Map에 똑같이 유용할 수 있습니다.

문자열 Iterator

JavaScript 문자열은 ECMAScript 5가 출시된 이후 천천히 배열과 비슷해졌습니다. 예를 들어, ECMAScript 5는 문자열의 문자에 액세스하기위한 대괄호 표기법을 사용합니다 (즉, 첫 번째 문자를 가져 오기 위해 text[0] 사용). 그러나 대괄호 표기법은 문자가 아닌 Code Unit에서 작동하므로 아래 예제에서 보여주는 것처럼 2 바이트 문자를 올바르게 액세스하는데 사용할 수 없습니다.

1
2
3
4
5
var message = "A ð ®· B";
for (let i=0; i < message.length; i++) {
console.log(message[i]);
}

이 코드는 괄호 표기법과 length 프로퍼티를 사용하여 반복하고 유니 코드 문자가 포함된 문자열을 출력합니다. 결과는 예상과 다릅니다.

1
2
3
4
5
6
A
(blank)
(blank)
(blank)
(blank)
B

두개의 바이는 두 개의 개별 Code Unit으로 취급되기 때문에 A와 B 사이에 4 개의 비어있는 행이 출력됩니다.

다행스럽게도 ECMAScript 6은 유니 코드를 완벽하게 지원하고 (2 장 참조) 기본 문자열 Iterator는 문자열 반복 문제를 해결하기 위한 시도입니다. 따라서 문자열의 기본 IteratorCode Unit이 아닌 문자단위로 작동합니다. 이 예제를 for-of 루프와 함께 기본 문자열 Iterator를 사용하도록 변경하면 보다 적절한 결과가 출력됩니다. 다음은 수정된 코드입니다.

1
2
3
4
5
var message = "A ð ®· B";
for (let c of message) {
console.log(c);
}

출력 결과는 다음과 같습니다.

1
2
3
4
5
A
(blank)
ð ®·
(blank)
B

이 결과는 문자로 작업할 때 기대했던 것과 더 비슷합니다. 루프는 유니 코드 문자뿐 아니라 나머지 문자도 모두 성공적으로 출력합니다.

NodeList Iterator

DOM (Document Object Model)에는 문서의 요소 컬렉션을 나타내는 NodeList 타입이 있습니다. JavaScript를 웹 브라우저에서 실행하는 사람들에게는 NodeList 객체와 Array의 차이점을 이해하는 것이 항상 약간 어려웠습니다. NodeList 객체와 Array는 항목의 수를 나타 내기 위해 length 프로퍼티를 사용하며, 둘 다 괄호 표기법을 사용하여 개별 항목에 액세스합니다. 그러나 내부적으로 NodeListArray는 완전히 다르게 작동하므로 많은 혼란을 겪습니다.

ECMAScript 6에 Default Iterator가 추가된 NodeList (ECMAScript 6 자체가 아닌 HTML 사양에 포함됨)의 DOM 정의에는 Array Default Iterator와 같은 방식으로 작동하는 Default Iterator가 포함되어 있습니다. 즉, for-of 루프 또는 객체의 Default Iterator를 사용하는 다른 부분에서 아래와같이 NodeList를 사용할 수 있습니다.

1
2
3
4
5
var divs = document.getElementsByTagName("div");
for (let div of divs) {
console.log(div.id);
}

이 코드는 getElementsByTagName()을 호출하여 document 객체의 모든 <div> 요소를 나타내는 NodeList를 검색합니다. for-of 루프는 각 요소를 반복하고 엘리먼트 ID를 출력하므로 표준 Array의 코드와 동일합니다.

Spread 연산자와 Non-Array Iterables

7 장에서 아래와 같이 SetArray로 변환하는 데 Spread 연산자 (...)를 사용할 수 있다는 점을 설명했습니다.

1
2
3
4
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]

이 코드는 Array 리터럴의 Spread 연산자를 사용하여 해당 Arrayset의 값으로 채웁니다. Spread 연산자는 모든 Iterable에서 작동하고 Default Iterator를 사용하여 포함할 값을 결정합니다. 모든 값은 Iterator에서 읽혀지고 Iterator에서 값이 리턴된 순서대로 Array에 삽입됩니다. 이 예제는 SetIterable이기 때문에 작동했습니다. 그리고 모든 Iterable에서 똑같이 작동 합니다. 다른 예를 살펴 보겠습니다.

1
2
3
4
let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "Nicholas"], ["age", 25]]

여기에서 Spread 연산자는 mapArrayArray로 변환합니다. Map에 대한 Default Iterator는 키-값 쌍을 반환하기 때문에 결과 Arraynew Map()호출 중에 전달된 Array처럼 보입니다.

Array 리터럴에서 원하는 만큼 여러번 Spread 연산자를 사용할 수 있으며 Iterable에서 여러 항목을 삽입하려는 곳이면 어디에서나 사용할 수 있습니다. 이러한 항목은 Spread 연산자의 위치에 있는 새로운 Array에 순서대로 나타납니다.

1
2
3
4
5
6
let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]

Spread 연산자는 smallNumbersbigNumbers의 값에서 allNumbers를 만드는 데 사용됩니다. 값은 allNumbers가 만들어질 때 Array가 추가되는 순서와 동일한 순서로 allNumbers에 배치됩니다. 먼저 0이오고 smallNumbers의 값이 뒤 따르고 bigNumbers의 값이옵니다. 그러나 원래 배열은 값이 allNumbers로 복사되었으므로 변경되지 않습니다.

Spread 연산자는 어떤 Iterable에서도 사용할 수 있기 때문에, IterableArray로 변환하는 가장 쉬운 방법입니다. 문자열을 문자 Array(Code Unit 아님) 및 브라우저의 NodeList 객체를 Node Array로 변환할 수 있습니다.

for-of 연산자와 Spread 연산자를 포함하여 Iterator가 작동하는 기본 사항을 이해 했으므로 이제는 Iterator를 좀 더 복잡한 용도로 살펴볼 차례입니다.

Iterator 고급 기능

IteratorGenertor를 이용하여 쉽게 만들고 기본 기능을 이용하여 Iterator를 쉽게 사용할수 있습니다. 그러나 Iterator는 단순히 값의 모음을 반복하는것 이외의 작업에 사용될 때 훨씬 강력합니다. ECMAScript 6을 개발하는 동안 제작자가 더 많은 기능을 추가하도록 독창적인 아이디어와 패턴이 많이 나타났습니다. 이러한 추가 기능중 일부는 함께 사용하면 흥미로운 상호 작용을 수행할 수 있습니다.

Iterator에 파라미터 넘기기

이 장 전반의 예제에서는 Iteratornext() 메서드를 통해 값을 전달받거나 Generatoryield를 사용하는 모습을 보여줬습습니다. 그러나 next() 메서드를 통해 Iterator에 파라미터를 전달할 수도 있습니다. 파라미터가 next() 메서드에 전달되면, 그 파라미터는 Generator 내부의 yield 문의 값이 됩니다. 이 기능은 비동기 프로그래밍과 같은 고급 기능에 중요합니다. 다음은 기본적인 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

next()의 첫 번째 호출은 전달된 파라미터가 사라지는 특별한 경우입니다. yield 문 앞에 접근할 수 있다면 next()에 전달된 파라미터가 yield에 의해 리턴된 값이되기 때문에, next()에 대한 첫 번째 호출의 파라미터는 Generator 함수의 첫 번째 yield 문장을 대체할 수 있습니다. 하지만 그건 불가능합니다. 그래서 next()가 처음 호출되었을 때 파라미터를 넘길 이유가 없습니다.

next()의 두 번째 호출에서 4값이 파라미터로 전달됩니다. 4Generator 함수 내에서 first 변수에 할당됩니다. 할당을 포함하는 yield 문에서 표현식의 오른쪽은 next()의 첫 번째 호출에서 평가되고 왼쪽은 함수가 계속 실행되기 전에 next()의 두 번째 호출에서 평가됩니다. next()의 두 번째 호출이 4로 전달되기 때문에, 그 값은 first에 할당되고 실행이 계속됩니다.

두 번째 yield는 첫 번째 yield의 결과를 사용하고 두개를 더합니다. 즉, 6의 값을 반환합니다. next()가 세번째로 호출될 때, 값 5가 파라미터로 전달됩니다. 이 값은 변수 second에 할당된 다음 yield 문에서 8을 반환하는데 사용됩니다.

실행이 Generator 함수 내에서 계속될 때마다 어떤 코드가 실행되고 있는지 고려하여 어떤 일이 일어나고 있는지 생각하는 것이 쉽습니다. 그림 8-1은 색상을 사용하여 실행중인 코드를 보여줍니다.


[그림 8-1]Generator 내부에서 코드 실행

노란색은 next()에 대한 첫 번째 호출과 Generator 내부에서 실행된 코드를 나타냅니다. 아쿠아색은 next(4)의 호출과 그 호출로 실행된 코드를 나타냅니다. 그리고 자주색은 next(5) 호출과 그 결과로 실행되는 코드를 나타냅니다. 까다로운 부분은 왼쪽면이 실행되기 전에 각 표현식의 오른쪽에있는 코드가 어떻게 실행되고 중지되는지 입니다. 이것은 일반 함수를 디버깅하는 것보다 Generator를 디버깅하는 것을 더 복잡하게 만드는 원인입니다.

지금까지 next() 메서드에 값이 전달될 때 yieldreturn처럼 작동할 수 있다는 것을 보았습니다. 그러나 이것만이 Generator 내부에서 수행할 수있는 유일한 실행이 아닙니다. Iterator로 인해 오류가 발생할 수도 있습니다.

Iterator에서 에러 던지기

Iterator에 데이터뿐만 아니라 오류 조건도 전달할 수 있습니다. IteratorIterator가 다시 시작할 때 오류를 발생 시키도록 지시하는 throw() 메서드를 구현하도록 선택할 수 있습니다. 이것은 비동기 프로그래밍을 위한 중요한 기능이고, 함수를 종료하는 두가지 방법인 리턴과 오류를 던지는 기능을 모방할 수 있어 Generator 내부의 유연성에도 중요합니다. Iterator가 처리를 계속할 때 던져 져야하는 throw()에 에러 객체를 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 2, then throw
yield second + 3; // never is executed
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // generator에서 에러를 던집니다.

이 예제에서 처음 두개의 yield 표현식은 평범한 것으로 평가되지만 throw()가 호출되면 let second가 평가되기 전에 에러가 발생합니다. 이로 인해 코드 실행이 오류를 직접 throw 하는 것과 비슷한 효과로 중지됩니다. 유일한 차이점은 오류가 발생하는 위치입니다. [그림 8-2]는 각 단계에서 어떤 코드가 실행되는지 보여줍니다.


[그림 8-2] Generator 내부에서 에러를 던집니다

이 그림에서 붉은색은 throw()가 호출될 때 실행되는 코드를 나타내며 빨간색 별은 오류가 Generator 내부로 던져지는 부분을 대략 나타냅니다. 처음 두개의 yield 문이 실행되고 throw()가 호출되면 다른 코드가 실행되기 전에 오류가 발생합니다.

이것을 알고있다면, try-catch 블록을 사용하여 Generator 내에서 에러를 잡을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // yield 4 + 2, then throw
} catch (ex) {
second = 6; // 에러시 다른 값을 할당함.
}
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 예제에서, 두 번째 yield 문은 try-catch 블록이 감싸고 있습니다. 이 yield는 에러 없이 실행되고, 다음 실행에서 어떤한 값이 second에 할당되기 전에 에러가 던져 지므로 catch 블록은 6의 값을 할당합니다. 그리고 실행은 다음 yield로 이동하여 9를 반환합니다.

흥미로운 일이 발생합니다. throw() 메서드는 next() 메서드와 마찬가지로 결과 객체를 리턴합니다. 오류가 Generator 내부에서 발견 되었기 때문에 코드 실행은 다음 yield로 이어지고 다음 값인 9가 반환됩니다.

Iterator에 대한 지시사항인 next()throw()를 생각하는 것이 도움이됩니다. next() 메서드는 Iterator에게 (주어진 값으로) 실행을 계속하도록 지시하고 throw()Iterator에게 오류를 던져 실행을 계속하도록 지시합니다. 그 시점 이후에 일어나는 일은 Generator 내부의 코드에 달려 있습니다.

next()throw() 메서드는 yield를 사용할 때 Iterator 안에서 실행을 제어하지만 return 문도 사용할 수 있습니다. 그러나return은 다음 절에서 보듯이 정규 함수와 약간 다르게 작동합니다.

Generator의 Return 문

Generator는 함수이기 때문에 return 문을 사용하여 일찍 종료하고 next() 메서드에 대한 마지막 호출의 반환 값을 지정할 수 있습니다. 이 장의 대부분의 예제에서, Iterator에서 next()를 마지막으로 호출하면 undefined가 반환되지만 다른 함수에서와 같이 return을 사용하여 값을 지정할 수 있습니다. Generator에서 return은 모든 처리가 완료되었음을 나타내므로 done 프로퍼티는true로 설정되고 값이 제공되면 value가 됩니다. 다음은 return을 사용하여 일찍 종료하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
function *createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 코드의 Generator에는 yield문 다음에 return 문장이 옵니다. return은 더 이상의 값이 없다는 것을 나타내므로 나머지 yield 문은 실행되지 않습니다 (도달할 수 없습니다).

리턴된 오브젝트의 value 필드에 값을 지정할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
function *createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

두 번째 next() 메서드 호출에서 value 프로퍼티에 값 42가 리턴됩니다.(done이 처음으로 true인 지점입니다.)
next()의 세번째 호출에 value 프로퍼티가 다시 undefined인 객체를 반환합니다. return으로 지정한 값은 value 프로퍼티가undefined로 재설정되기 전에 반환된 객체에서만 한번 사용할 수 있습니다.

Spread 연산자와 for-ofreturn 문에 의해 지정된 값을 무시합니다. donetrue이면 바로value를 읽지 않고 멈춥니다. 그러나 Generator를 위임할 때는 Iterator 리턴 값이 유용합니다.

Generator 위임

경우에 따라 두 개의 Iterator 값을 하나로 결합하는 것이 유용할 수 있습니다. Generator는 별표 (*) 문자로 yield라는 특수 형식을 사용하여 다른 Iterator에 위임할 수 있습니다. Generator 정의에서와 같이 *가 나타나는 위치는 *yield 키워드와 Generator 함수 이름 사이에있는 한 중요하지 않습니다. 다음은 그 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function *createNumberIterator() {
yield 1;
yield 2;
}
function *createColorIterator() {
yield "red";
yield "green";
}
function *createCombinedIterator() {
yield *createNumberIterator();
yield *createColorIterator();
yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

위 예제에서, createCombinedIterator() GeneratorcreateNumberIterator()에서 리턴된 Iterator에 먼저 위임한 다음createColorIterator()에서 리턴된 Iterator에 위임합니다. createCombinedIterator()에서 반환된 Iterator는 바깥쪽에서 보기에 일관된 Iterator로 보여집니다. next()에 대한 각각의 호출은 createNumberIterator()createColorIterator()에 의해 생성된 Iterator가 비게 될 때까지 적절한 Iterator에 위임됩니다. 그런 다음 최종 yield가 실행되어 true를 반환합니다.

Generator 위임을 통해 Generator 리턴값을 더 많이 사용할 수도 있습니다. 이것은 반환된 값을 액세스하는 가장 쉬운 방법이며 복잡한 작업을 수행하는데 매우 유용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

여기서 createCombinedIterator() Generator createNumberIterator()에 위임하고 result에 리턴값을 할당합니다. createNumberIterator()return 3을 포함하기 때문에 리턴값은 3입니다. result 변수는 createRepeatingIterator()에 같은 문자열을 yield 하는 횟수(이 경우에는 3 번)를 나타내는 인자로 전달됩니다.

next() 메서드에 대한 호출에서 3 값은 절대로 출력되지 않았습니다. 지금은 createCombinedIterator() Generator 안에만 존재합니다. 그러나 다음과 같이 또 다른 yield 문을 추가하여 그 값을 출력할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield result;
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

이 코드에서 여분의 yield 문은 리턴된 값을 createNumberIterator() Generator에서 명시적으로 출력합니다.

리턴값을 사용하는 Generator 위임은 특히 비동기 작업과 함께 사용할 때 매우 흥미로운 가능성을 가지는 매우 강력한 패러다임입니다.

문자열에 yield *를 직접 사용할 수 있습니다 (예 :yield * "hello"). 이 경우 문자열의 기본 Iterator가 사용됩니다.

비동기(Asynchronous) 작업 실행

Generator의 흥미로운 점은 비동기 프로그래밍과 직접 관련이 있습니다. JavaScript의 비동기 프로그래밍은 양날의 칼입니다. 간단한 작업은 비동기식으로 수행하기 쉽고 복잡한 작업은 코드에서 내용 역할을 하게됩니다. Generator는 실행 중간에 코드를 효과적으로 일시 중지할 수 있으므로 비동기 처리와 관련된 많은 가능성을 가지고 있습니다.

비동기 작업을 수행하는 전통적인 방법은 콜백이 있는 함수를 호출하는 것입니다. 예를 들어, Node.js의 디스크에서 파일을 읽는 것을 생각해
보겠습니다.

1
2
3
4
5
6
7
8
9
10
let fs = require("fs");
fs.readFile("config.json", function(err, contents) {
if (err) {
throw err;
}
doSomethingWith(contents);
console.log("Done");
});

fs.readFile() 메서드는 읽을 파일 이름과 콜백 함수를 가지고 호출됩니다. 작업이 끝나면 콜백 함수가 호출됩니다. 콜백은 오류가 있는지 검사하고 그렇지 않은 경우 반환된 contents을 처리합니다. 이 작업은 작거나, 유한한 수의 비동기 작업을 완료하는데는 괜찮지만 콜백을 중첩하거나 일련의 비동기 작업을 순서대로 수행해야하는 경우에는 복잡해집니다. 이부분이 Generatoryield가 도움이되는 곳입니다.

간단한 작업 실행

yield는 실행을 멈추고 다시 시작하기 전에 next() 메서드가 호출되기를 기다리기 때문에 콜백을 관리하지 않고 비동기 호출을 구현할 수 있습니다. 시작하려면 Generator를 호출하고 다음과 같이 Iterator를 시작할 수있는 함수가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function run(taskDef) {
// iterator를 만들고 다른곳에서 사용할 수 있게 합니다.
let task = taskDef();
// 태스크 시작
let result = task.next();
// next() 호출을 계속하는 재귀 함수
function step() {
// 더해야 할 일이 있다면
if (!result.done) {
result = task.next();
step();
}
}
// process를 시작
step();
}

run() 함수는 태스크의 정의 (Generator 함수)를 파라미터로 받아들입니다. Generator를 호출하여 Iterator를 만들고 Iteratortask에 저장합니다. task 변수는 함수 외부에 있으므로 다른 함수가 접근할 수 있습니다(뒷부분에서 설명 합니다.). next()에 대한 첫 번째 호출은 Iterator를 시작하고 결과는 나중에 사용하기 위해 저장됩니다. step() 함수는 result.done이 false인지 검사하고, 만약 그렇다면 재귀적으로 자신을 호출하기 전에 next()를 호출합니다. next()를 호출할 때마다 결과 값이 result에 저장됩니다.이 값은 항상 최신 정보를 포함하도록 덮어 쓰여집니다. 처음 step()의 호출은 더 수행할 태스크가 있는지를 알기 위해 result.done 변수를 확인 합니다.

위에 구현한 run()을 다음과 같이 여러 yield 문이 포함된 Generator로 실행할 수 있습니다.

1
2
3
4
5
6
7
run(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});

이 예제는 단순히 콘솔에 세 개의 숫자를 출력하는데, 간단히 next()에 대한 모든 호출이 이루어지는 것을 보여줍니다. 그러나 단지 두 번 yield하는 것은 별로 유용하지 않습니다. 다음 단계는 Iterator 안팎으로 값을 전달하는 것입니다.

데이터를 가진 태스크 실행하기

태스크 실행에 데이터를 전달하는 가장 쉬운 방법은 yield에 의해 지정된 값을 next() 메서드에 대한 호출에 전달하는 것입니다. 이렇게하려면 이 코드에서와 같이 result.value 만 전달하면됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function run(taskDef) {
// iterator를 만들고 다른곳에서 사용할 수 있게 합니다.
let task = taskDef();
// 태스크 시작
let result = task.next();
// next() 호출을 계속하는 재귀 함수
function step() {
// 더해야 할 일이 있다면
if (!result.done) {
result = task.next(result.value);
step();
}
}
// process 시작
step();
}

이제 result.value가 파라미터로 next()에 전달되었으므로 다음과 같이 yield 호출간에 데이터를 전달할 수 있습니다.

1
2
3
4
5
6
7
run(function*() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});

이 예제는 콘솔에 두 개의 값을 출력합니다(1과 4). 값 1은 yield 1에서 나오는데, 1은 value 변수로 바로 전달됩니다. 4는 value에 3을 더하고 그 결과를 value에 전달함으로써 계산됩니다. 데이터가 yield호출 사이에서 흐르고 있으므로 비동기 호출을 허용하려면 작은 변경만 하면됩니다.

비동기 태스크 실행

앞의 예제는 정적 데이터가 yield 호출 사이에서 왔다 갔다했지만 비동기 프로세스를 기다리는 것은 약간 다릅니다. 태스크 러너는 콜백 및 그 사용 방법을 알아야합니다. 그리고 yield 표현식은 값을 태스크 러너로 전달하기 때문에 어떤 함수 호출이라도 호출이 태스크 러너가 기다려야하는 비동기 연산임을 나타내는 값을 리턴해야 함을 의미합니다.

다음은 값이 비동기 작업임을 알리는 한 가지 방법입니다.

1
2
3
4
5
function fetchData() {
return function(callback) {
callback(null, "Hi!");
};
}

이 예제의 목적을 위해, 태스크 러너에 의해 호출되는 모든 함수는 callback을 실행하는 함수를 리턴할 것이다. fetchData() 함수는 콜백 함수를 파라미터로 받아들이는 함수를 리턴한다. 반환된 함수가 호출되면, 단일 데이터 ( "Hi!"문자열)로 콜백 함수를 실행합니다. callback 파라미터는 콜백을 실행하는 것이 기본 Iterator와 정확하게 상호 작용 하는지를 확인하기 위해 태스크 러너로부터 올 필요가있습니다. fetchData() 함수는 동기식이지만, 다음과 같이 약간의 지연만으로 콜백을 호출하여 쉽게 비동기식으로 확장할 수 있습니다.

1
2
3
4
5
6
7
function fetchData() {
return function(callback) {
setTimeout(function() {
callback(null, "Hi!");
}, 50);
};
}

이 버전의 fetchData()는 콜백을 호출하기 전에 50ms의 지연을 가져와 이 패턴이 동기 및 비동기 코드에서 똑같이 잘 작동 함을 보여줍니다. yield를 사용하여 호출하려는 각 함수가 동일한 패턴을 따르는지 확인해야 합니다.

함수가 비동기 프로세스라는 신호를 보내는 방법을 잘 이해하면 태스크 러너를 수정하여 해당 사실을 고려할 수 있습니다. result.value가 함수 일 때마다, 태스크 러너는 그 값을 next() 메서드로 전달하는 대신에 실행할 것입니다. 다음은 업데이트된 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function run(taskDef) {
// iterator를 만들고 다른곳에서 사용할 수 있게 합니다.
let task = taskDef();
// 태스크 시작
let result = task.next();
// next() 호출을 계속하는 재귀 함수
function step() {
// 더해야 할 일이 있다면
if (!result.done) {
if (typeof result.value === "function") {
result.value(function(err, data) {
if (err) {
result = task.throw(err);
return;
}
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// 프로세스 시작
step();
}

result.value가 (===연산자로 검사된) 함수이면 콜백 함수가 호출됩니다. 이 콜백 함수는 가능한 오류를 첫 번째 인수 (err)로 전달하고 결과를 두 번째 인수로 전달하는 Node.js 규칙을 따릅니다. err가 있어 오류가 발생하면 task.throw()task.next()대신에 오류 객체와 함께 호출되므로 정확한 위치에 오류가 발생합니다. 오류가 없으면 datatask.next()에 전달되고 그 결과가 저장됩니다. 그런 다음 step()이 호출되어 프로세스가 계속 진행됩니다. result.value가 함수가 아니라면 next() 메서드에 직접 전달됩니다.

이 새로운 버전의 태스크 러너는 모든 비동기 태스크에 대한 준비가 되어 있습니다. Node.js에서 파일로부터 데이터를 읽으려면, 이 섹션의 시작 부분에서 fetchData() 함수와 유사한 함수를 반환하는 fs.readFile()을 감싸는 래퍼를 생성해야합니다.

1
2
3
4
5
6
7
let fs = require("fs");
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}

readFile() 메서드는 파라미터인 파일명을 받아들이고 콜백을 호출하는 함수를 반환합니다. 콜백은 fs.readFile() 메서드에 직접 전달되며, 메서드는 완료시 콜백을 실행합니다. 다음과 같이 yield를 사용하여 이 작업을 실행할 수 있습니다.

1
2
3
4
5
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});

이 예제는 주 코드에 콜백을 표시하지 않고 비동기 readFile() 연산을 수행합니다. yield와는 별도로, 코드는 동기 코드와 동일하게 보입니다. 비동기 작업을 수행하는 함수가 모두 동일한 인터페이스를 준수하는 한 동기 코드와 같은 로직을 작성할 수 있습니다.

물론,이 예제에서 사용된 패턴에는 단점이 있습니다. 즉, 함수를 반환하는 함수가 비동기인지 항상 확신할 수는 없습니다. 지금 당장은 실행중인 태스크 뒤에있는 이론을 이해하는 것이 중요합니다. Promise를 사용하면 비동기 작업을 예약하는 보다 강력한 방법이 제공되며 11 장에서는 이 주제에 대해 자세히 설명합니다.

요약

Iterator는 ECMAScript 6의 중요한 부분이며 언어의 몇 가지 핵심 요소에 존재합니다. 표면적으로 Iterator는 간단한 API를 사용하여 일련의 값을 반환하는 방법을 제공합니다. 그러나 ECMAScript 6에서 Iterator를 사용하는 훨씬 더 복잡한 방법이 있습니다.

Symbol.iterator Symbol은 객체의 기본 Iterator를 정의하는데 사용됩니다. Built-in 객체와 개발자 정의 객체는 모두 이 Symbol을 사용하여 Iterator를 반환하는 메서드를 제공할 수 있습니다. Symbol.iterator가 객체에 제공되면 객체는 Iterable한 것으로 간주됩니다.

for-of 루프는 Iterable를 사용하여 루프에서 일련의 값을 반환합니다. for-of를 사용하면 더 이상 값을 추적할 필요가 없고 루프가 끝나는 시점을 제어할 필요가 없으므로 전통적인 for 루프를 반복하는 것보다 쉽습니다. for-of 루프는 Iterator에서 더 이상 값이 없을 때까지 모든 값을 자동으로 읽은 다음 종료합니다.

for-of를 더 쉽게 사용하기 위해 ECMAScript 6의 많은 타입에는 기본 Iterator가 있습니다. 컬렉션, 즉 Array, MapSet과 같은 모든 컬렉션 유형에는 내용에 쉽게 액세스할 수 있도록 설계된 Iterator가 있습니다. 문자열에는 기본 Iterator가 있어 Code unit이 아닌 문자열의 문자를 쉽게 반복할 수 있습니다.

Spread 연산자는 모든 Iterable 함수에서 작동하며 IterableArray로 쉽게 변환합니다. 변환은 Iterator에서 값을 읽어 Array에 개별적으로 삽입하여 작동합니다.

Generator는 호출될 때 Iterator를 자동으로 생성하는 특수 함수입니다. Generator 정의는 별표(*) 문자로 표시되고 yield 키워드를 사용하여 next() 메서드를 연속적으로 호출할 때 반환할 값을 나타냅니다.

Generator 위임은 새로운 Generator에서 기존 Generator를 재사용 하도록 함으로써 Iterator 동작을 잘 캡슐화하도록 합니다. yield 대신 yield *를 호출하여 다른 Generator의 기존 Generator를 사용할 수 있습니다. 이 프로세스를 통해 여러 Iterator의 값을 반환하는 Iterator를 만들 수 있습니다.

아마도 GeneratorIterator 중 가장 흥미로운 부분은 보다 깨끗한 비동기 코드를 생성할 수 있다는 것입니다. 콜백을 사용하는 대신 동기식으로 보이는 코드가 실제로 yield를 사용하면 비동기 작업으로 완료될 때까지 대기합니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-iterators-and-generators]

참고

공유하기