ES6에서 도입된 이터레이터(iterator)와 제너레이터(generator)는 자주 들어봤지만 실제 개발할땐 거의 사용하지 않던 부분이었다. 명확한 개념 이해를 위해 정리해봄!
1. 이터레이터란?
이터레이터는 반복을 위해 설계된 특정 인터페이스가 있는 객체이다. 두 개의 속성 {value, done}
을 반환하고, next 메서드를 가진다. MDN에서는 '시퀀스를 정의하고 종료시의 반환값을 잠재적으로 정의하는 객체'라고 되어 있다. Array.prototype.values()를 사용하면 배열의 각 인덱스에 대한 값을 가지는 새로운 Array Iterator 객체를 반환하여 이터레이터를 만들 수 있다.
const book = [
'a', 'b', 'c', 'd',
];
const it = book.values();
it.next(); // {value: 'a', done: false}
it.next(); // {value: 'b', done: false}
it.next(); // {value: 'c', done: false}
it.next(); // {value: 'd', done: false}
it.next(); // {value: undefined, done: true}
it.next(); // {value: undefined, done: true}
이터레이터를 생성하면 next() 메소드를 반복적으로 호출하여 명시적으로 반복시킬 수 있다. value
프로퍼티는 다음 시퀀스의 값을 반환하고, done
은 시퀀스의 마지막 값이 산출(소비)되었는지 여부를 boolean으로 반환한다. 마지막 값이 반환된 후에 next()를 호출하면 메서드는 done을 true로 리턴한다.
자바스크립트에서 반복되는 열거 가능(enumerable)한 속성이 있는 객체를 '이터러블(iterable)하다'고 한다. 이터러블한 객체에는 string, array, map, set 등이 있다. 이터러블한 객체의 프로토타입 객체에는 모두 Symbol.iterator 메서드가 있다.
또한 이터레이터가 배열과 다른 점은, 배열은 전체 값이 할당되어야 하지만 이터레이터는 무한대로 표현될 수 있다는 것이다.
2. 이터레이터를 직접 구현해보기
이터레이터와 같은 역할을 하는 함수를 직접 구현해보자.
function createIterator(items) {
let i = 0;
return {
next: function() {
const done = (i >= items.length);
const value = !done ? items[i++] : undefined;
return {
done: done,
value: value,
};
}
};
}
const 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 }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
위 코드에서 iterator.next()
로 반복되다가 value: 3
으로 마지막 시퀀스의 값을 반환하고, 그 다음으로 호출된 next()
에서는 done: true
가 찍히게 된다.
이렇게 직접 이터레이터를 구현할 수도 있지만, ES6에서는 이터레이터 객체를 쉽게 만들 수 있는 생성자를 제공한다.
3. 제너레이터(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
function 다음에 붙은 별표(=애스터리스크, * )는 이 함수가 제너레이터 함수라는 의미이다. 또한 yield 키워드는 next()가 호출될 때 이터레이터가 리턴할 결과 값을 리턴될 순서대로 지정한다.
제너레이터 함수는 모든 값을 리턴하지 않고, 첫번째 시퀀스를 돈 후에 다음 next()가 호출될 때까지 실행을 멈춘다. 위의 함수 createIterator를 실행했을 때, 최초 실행 시점인 let iterator = createIterator();
컨텍스트에서는 함수 내부의 어떠한 코드도 실행되지 않고, 대신 생성자라고 불리는 반복자 타입이 반환된다. 그 뒤에 iterator.next()
형태로 생성자의 next 메소드를 호출하게 되면, 생성자 함수는 yield 1를 실행하고 value: 1
을 리턴한다.
다시 말해 제너레이터가 일반 함수와 다른 점은 함수의 실행을 제어할 수 있다는 점이다. 또한 실행된 yield문이 제너레이터의 마지막 시퀀스이더라도 함수가 끝나는 것이 아니라 계속해서 next()를 호출할 수 있다. 이 경우 done: true
가 될 것이다.
제너레이터는 호출한 즉시 실행되지 않는다. 대신 이터레이터를 반환하고, 이터레이터의 next 메서드를 호출함에 따라 실행된다.
위의 예시에서 createIterator 제너레이터는 이터레이터를 반환하므로 for...of 루프에서 쓸 수 있다.
let iterator = createIterator();
for (const a of iterator) {
console.log(a);
}
참고 :
MDN- Iterators and generators
(https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Iterators_and_Generators)
ECMAScript 6 Iterator와 Generator
(https://infoscis.github.io/2018/01/31/ecmascript-6-iterators-and-generators/)