이터레이터와 제너레이터는 ES6에 새로 도입된 중요한 개념이다.
이터레이터는 '지금 어디 있는지' 파악할 수 있도록 돕는다는 면에서 일종의 책갈피와 비슷한 개념이다. 배열은 이터러블 객체의 좋은 예로 배열의 여러 요소에 이터레이터를 사용할 수 있다.
const book = [
'1 page',
'2 page',
'3 page',
'4 page',
'5 page',
'6 page',
'7 page',
];
const it = book.values(); // 이터레이터를 it으로 줄여 씀
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
// {value: "1 page", done: false}
// {value: "2 page", done: false}
// {value: "3 page", done: false}
// {value: "4 page", done: false}
// {value: "5 page", done: false}
// {value: "6 page", done: false}
// {value: "7 page", done: false}
// {value: undefined, done: true}
next() 메서드를 사용하면 배열을 읽기 시작하고, 메서드가 반환하는 객체에는 value 프로퍼티와 done 프로퍼티가 있다. done 프로퍼티는 마지막 요소를 읽으면 true를 반환한다. 이터레이터는 더 진행할 것이 없으면 value는 undefined가 되지만, next는 계속 호출할 수 있다.
it.next()를 호출하는 중간에 다른 일을 할 수 있다.
for...of 의 경우 인덱스 없이 루프를 실행 할 수 있는데 이터레이터가 제공되기 때문이다.
이터레이터와 while 루프를 사용해서 for...of 루프를 흉내 내 보자.
const it = book.values();
let current = it.next();
while(!current.done) {
console.log(current.value);
current = it.next();
}
이터레이터는 모두 독립적이므로 새 이터레이터를 만들 때마다 처음에서 시작한다. 그리고 각각 다른 요소를 가리키는 이터레이터 여러 개를 동시에 사용할 수 도 있다.
const it1 = book.values();
const it2 = book.values();
// it1 두 페이지 읽기
console.log(it1.next()); // {value: "1 page", done: false}
console.log(it1.next()); // {value: "2 page", done: false}
// it2 한 페이지 읽기
console.log(it2.next()); // {value: "1 page", done: false}
// it1 한 페이지 더 읽기
console.log(it1.next()); // {value: "3 page", done: false}
it1 과 it2 이터레이터가 서로 독립적이며 같은 배열에서 따로따로 움직일 수 있다는 것을 보여준다.
이터레이터는 무한한 데이터에도 사용할 수 있다.
피보나치 수열은 무한히 계속되고, 프로그램에서는 몇 번재 숫자까지 계산해야 할지 알 수 없으므로 이터레이터를 사용하기에 알맞다. 이전 예제와의 차이점은 이터레이터가 done에서 절대 true를 반환하지 않는다는 것이다.
const Posts = () => {
class Fibonacci {
[Symbol.iterator]() {
let a = 0, b = 1;
return {
next() {
let rval = { value: b, done: false };
b += a;
a = rval.value;
return rval;
}
};
}
}
for...of 루프로 Fibonacci 인트턴스를 계산하면 무한 루프에 빠지게 되므로, 10회 계산한 뒤 break 문으로 빠져나오게 한다.
const fib = new Fibonacci();
let i = 0;
for(let n of fib) {
console.log(n);
if(++i > 9) break;
}
제너레이터란 이터레이터를 사용해 자신의 실행을 제어하는 함수로, 일반적인 함수는 매개변수를 받고 값을 반환하지만, 호출자는 매개변수 외에는 함수의 실행을 제어할 방법이 전혀 없다. 함수를 호출하면 그 함수가 종료될 때까지 제어권을 완전히 넘기는 것이다.
하지만 제너레이터는 그렇지 않고, 두 가지 새로운 개념을 도입했다.
하나는 함수의 실행을 개별적 단계로 나눔으로써 함수의 실행을 개별적 단계로 나눠 함스의 실행을 제어하는 것.
다른 하나는 실행 중인 함수와 통신한다는 것이다.
제너레이터는 두 가지 예외를 제외하면 일반적인 함수와 같다.
제너레이터를 만들 때는 function 키워드 뒤에 애스터리크스(*)를 붙인다.
function* rainbow() {
yield 'red';
yield 'orange';
yield 'yellow';
yield 'green';
yield 'blue';
yield 'indigo';
yield 'violet';
}
const it = rainbow();
it.next();
it.next();
it.next();
it.next();
it.next();
it.next();
it.next();
it.next();
rainbow 제너레이터는 이터레이터를 반환하므로 for...of 루프에서 쓸 수 있다.
for(let color of rainbow()) {
console.log(color);
}
무지개 색이 콘솔에 입력된다.
제너레이터와 호출자 사이에서 양방향 통신은 yield 표현식을 통해 이루어진다. 표현식은 값으로 평가되고 yield는 표현식이므로 반드시 어떤 값으로 평가된다. yield 표현식의 값은 호출자가 제너레이터의 이터레이터에서 next를 호출할 때 제공하는 매개변수이다.
function* interrogate() {
const name = yield "What is yout name?";
const color = yield "What is your favorite color?";
return `${name}'s favorite color is ${color} `;
}
이 제너레이터를 호출하면 이터레이터를 얻는다.
next행을 호출하면 첫 번째 행을 실행하려 하는데 yield 표현식이 들어 있으므로 제너레이터는 반드시 제어권을 호출자에게 넘겨야 한다.
let it = interrogate();
it.next() // {value: "What is your name?", done: false}
it.next("승진") // {value: "What is your favorite color?", done: false}
it.next("파랑") // {value: "승진's favorite color is 파랑", done: true}
호출자가 제너레이터에게 정보를 전달하므로, 제너레이터는 그 정보에 따라 자신의 동작 방식 자체를 바꿀 수 있어 유용하게 사용할 수 있다.
제너레이터에서 return 문을 사용하면 위치와 관계없이 done은 true가 되고, value 프로퍼티는 return이 반환하는 값이 된다.
function* abc() {
yield 'a';
yield 'a';
return 'c';
}
const it = abc();
it.next(); // { value: 'a', done: false }
it.next(); // { value: 'b', done: false }
it.next(); // { value: 'c', done: true }
제너레이터를 사용할 때는 보통 done이 true일 경우 value 프로퍼티를 무시하게 된다. 예를 들어 이 제너레이터를 for...of 루프에서 사용하면 c는 절대 출력되지 않는다.
for(let l of abc()) {
console.log(l);
}
이터레이터로 할 수 있는 일은 ES6 이전에도 모두 할 수 있었으므로, 새로운 기능이 추가된 것은 아니다. 중요하면서도 자주 사용되는 패턴은 표준화했다는 점이 중요하고, 제너레이터를 사용하면 함수를 훨씬 더 유연하고 효율적으로 사용할 수 있다.
이제 함수를 호출하는 부분에서 데이터를 제공하고 호출한 함수가 완료될 때 까지 기다렸다가 반환값을 받는다는 사고방식에 얽매일 필요가 없다.