이터레이터는 '지금 어디 있는지' 파악할 수 있도록 돕는다는 면에서 일종의 책갈피와 비슷한 개념입니다. 배열은 이터러블 객체의 좋은 예입니다. 배열에는 여러 요소가 들어 있으므로, 이터레이터를 사용할 수 있습니다. book이란 배열이 있고, 이 배열의 각 요소는 책의 한 페이지를 나타내는 문자열이라고 합시다.
const book = [
"Twinke, twinkle, little bat!",
"How I wonder what you're at!",
"Up above the world you fly,",
"Like a tea tray in the sky.",
"Twinkle, twinkle, little bat!",
"How I wonder what you're at!",
]
이제 book 배열에 values 메서드를 써서 이터레이터를 만들 수 있습니다.
const it = book.values()
이터레이터(보통 it이라고 줄여 씁니다)는 책갈피지만, 이 책에만 사용할 수 있습니다. '읽기 시작'하려면 이터레이터의 next 메서드를 호출합니다. 이 메서드가 반환하는 객체에는 value 프로퍼티(지금 보이는 페이지)와 done 프로퍼티(마지막 페이지를 읽으면 true로 바뀌는)가 있습니다.
it.next() // {value: "Twinkle, twinkle, little bat!", done: false}
it.next() // {value: "How I wonder what you're at!", done: false}
it.next() // {value: "Up above the world you fly,", done: false}
it.next() // {value: "Like a tea tray in the sky.", done: false}
it.next() // {value: "Twinkle, twinkle, little bat!", done: false}
it.next() // {value: "How I wonder what you're at!", done: false}
it.next() // {value: undefined, done: true}
it.next() // {value: undefined, done: true}
it.next() // {value: undefined, done: true}
여기는 중요한 점이 몇 개 있습니다. 첫째, next에서 책의 마지막 페이지를 반환헀다 해서 끝난 것은 아니란 겁니다. 이터레이터는 책을 읽는 것보다 훨씬 다양한 상황에서 쓰일 수 있고 끝나는 시점을 간단히 결정할 수는 없습니다. 더 진행할 것이 없으면 value는 undefined가 되지만, next는 계속 호출할 수 있습니다. 물론 그렇다고 결과가 바뀌는건 아닙니다. 일단 이터레이터가 끝까지 진행하면 뒤로 돌아가서 다른 데이터를 제공할 수는 없습니다.
이 배열의 요소를 나열하는 것이 목적이라면 for 루프나 for...of 루프를 쓸 수 있습니다. for 루프의 원리는 간단합니다. 배열 요소의 인덱스는 숫자형이고 순차적으로 인덱스 변수를 써서 해당하는 배열 요소에 접근할 수 있습니다. 하지만 for...of 루프는 어떻게 된 걸까요? 인덱스 없이 어떻게 루프를 실행할 수 있었을까요? 답은 이터레이터입니다. 이터레이터만 제공할 수 있다면 무엇이든 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으로 두 페이지를 읽습니다.
it1.next() // {value: "Twinkle, twinkle, little bat!", done: false}
it1.next() // {value: "How I wonder what you're at!", done: false}
//it2로 한페이지를 읽습니다.
it2.next() // {value: "Twinkle, twinkle, little bat!", done: false}
//it1으로 한 페이지를 더 읽습니다.
it1.next() // {value: "Up above the world you fly,", done: false}
이터레이터는 그 자체로 크게 쓸모가 있다기보다는, 더 쓸모 있는 동작이 가능해지도록 한다는 데 의미가 있습니다. 이터레이터 프로토콜은 모든 객체를 이터러블 객체로 바꿀 수 있습니다. 메시지에 타임스탬프를 붙이는 로그 클래스가 필요하다고 생각해 봅시다.
class Log {
constructor() {
this.messages = []
}
add(message) {
this.messages.push({message,timestamp: Date.now()})
}
}
로그를 기록한 항목을 순회하고 싶다면 어떻게 해야할까요? 이터레이션 프로토콜을 사용하면 가능합니다. 이터레이션 프로토콜은 클래스에 심볼 메서드 Symbol.iterator가 있고 이 메서드가 이터레이터처럼 동작하는 객체, 즉 value와 done 프로퍼티가 있는 객체를 반환하는 next 메서드를 가진 객체를 반환한다면 그 클래스의 인스턴스는 이터러블 객체라는 뜻입니다. Log 클래스에 Symbol.iterator 메서드를 추가합니다.
class Log {
constructor() {
this.messages = []
}
add(message) {
this.messages.push({message,timestamp: Date.now()})
}
[Symbol.iterator](){
return this.messages.values()
}
}
이제 Log 인스턴스를 배열처럼 순회할 수 있습니다.
const log = new Log()
log.add("first day at sea")
log.add("spotted whale")
log.add("spotted another vessel")
// ...
// 로그를 배열처럼 순회합니다!
for(let entry of log) {
console.log(`${entry.message} @ ${entry.timestamp}`)
}
다음과 같이 직접 이터레이터를 만들 수도 있습니다.
class Log {
//...
[Symbol.iterator](){
let i = 0
const messages = this.messages
return {
next() {
if(i>=messages.length) return {value: undefined, done: true}
return {value: messages[i++], done: false}
}
}
}
}
이터레이터는 무한한 데이터에도 사용할 수 있습니다.
피보나치 수열은 무한히 계속되고, 프로그램에서는 몇 번째 숫자까지 계산해야 할지 알 수 없으므로 이터레이터를 사용하기에 알맞습니다. 차이점은 이터레이터가 done에서 절대 true를 반환하지 않는다는 것 뿐입니다.
class FibonacciSequence {
[Symbol.iterator](){
let a = 0, b = 1
return {
next() {
let rval = {value: b, done: false}
b += a
a = rval.value
return rval
}
}
}
}
for...of 루프로 FibonacciSequence 인스턴스를 계산하면 무한 루프에 빠집니다. 피보나치 수열은 무한하니까요. 무한 루프에 빠지지 않도록 10회 계산한 뒤 break 문으로 빠져나옵니다.
const fib = new FibonacciSequence()
le i = 0
for(let n of fib) {
console.log(n)
if(++i>9) break
}
이터레이터를 사용해 자신의 실행을 제어하는 함수입니다. 일반적인 함수는 매개변수를 받고 값을 반환하지만, 호출자는 매개변수 외에는 함수의 실행을 제어할 방법이 전혀 없습니다. 함수를 호출하면 그 함수가 종료될 때까지 제어권을 완전히 넘기는 겁니다. 제너레이터에서는 그렇지 않습니다.
제너레이터는 두 가지 새로운 개념을 도입했습니다. 하나는 함수의 실행을 개별적 단계로 나눔으로써 함수의 실행을 제어한다는 것입니다. 다른 하나는 실행 중인 함수와 통신한다는 것입니다.
제너레이터는 두 가지 예외를 제외하면 일반적인 함수와 같습니다.
제너레이터를 만들 때는 function 키워드 뒤에 애스터리스크(*)를 붙입니다. 이것을 제외하면 문법은 일반적인 함수와 같습니다. 제너레이터에서는 return 외에 yield 키워드를 쓸 수 있습니다.
function* rainbow() {
yield 'red'
yield 'orange'
yield 'yellow'
yield 'green'
yield 'blue'
yield 'indigo'
yield 'violet'
}
제너레이터를 호출하면 이터레이터를 얻습니다. 함수를 호출한 다음 이터레이터를 써서 단계별로 진행합니다.
const it = rainbow()
it.next() // {value: "red", done: false}
it.next() // {value: "orange", done: false}
it.next() // {value: "yellow", done: false}
it.next() // {value: "green", done: false}
it.next() // {value: "blue", done: false}
it.next() // {value: "indigo", done: false}
it.next() // {value: "violet", done: false}
it.next() // {value: undefined, done: true}
rainbow 제너레이터는 이터레이터를 반환하므로 for...of 루프에서 쓸 수 있습니다.
for(let color of rainbow()) {
console.log(color)
}
이 코드를 실행하면 무지개의 색깔이 모두 콘솔에 기록됩니다.
통신은 yield 표현식을 통해 이루어집니다. 표현식은 값으로 평가되고 yield는 표현식이므로 반드시 어떤 값으로 평가됩니다. yield 표현식의 값은 호출자가 제너레이터의 이터레이터에서 next를 호출할 때 제공하는 매개변수입니다.
function* interrogate(){
const name = yield "What is your name?"
const color = yield "What is your favorite color?"
return `${name}'s favorite color is ${color}.`
}
이 제너레이터를 호출하면 이터레이터를 얻습니다. 그리고 제너레이터의 어떤 부분도 아직 실행하지 않은 상태입니다. next를 호출하면 제너레이터는 첫 번째 행을 실행하려 합니다. 하지만 그 행에는 yield 표현식이 들어 있으므로 제너레이터는 반드시 제어권을 호출자에게 넘겨야 합니다. 제너레이터의 첫 번째 행이 완료되려면 호출자가 next를 다시 호출해야 합니다. 그러면 name은 next에서 전달하는 값을 받습니다.
const it =- interogate()
it.next() // {value: "What is your name?", done: false}
it.next('Ethan') // {value: "What is yout favorite color?", done: false}
it.next('orange') // {value: "Ethan's favorite color is orange, done: true}
제너레이터를 활용하면 호출자가 함수의 실행을 제어할 수 있어서 아주 유용하게 쓸 수 있습니다. 호출자가 제너레이터에 정보를 전달하므로, 제너레이터는 그 정보에 따라 자신의 동작 방식 자체를 바꿀 수 있습니다.
yield문은 제너레이터의 마지막 문이라도 제너레이터를 끝내지 않습니다. 제너레이터에서 return 문을 사용하면 그 위치와 관계없이 done은 true가 되고, value 프로퍼티는 return이 반환하는 값이 됩니다.
function*abc(){
yield 'a'
yield 'b'
yield 'b'
}
const it = abc()
it.next() // {value: 'a', done: false}
it.next() // {value: 'b', done: false}
it.next() // {value: 'c', done: true}
이런 동작 방식이 정확하기는 하지만, 제너레이터를 사용할 때는 보통 done이 true이면 value 프로퍼티에 주의를 기울이지 않는다는 점을 주의하세요.
// 'a'와 'b'는 출력되지만 'c'는 출력되지 않습니다.
for(let l of abc()){
console.log(l)
}