이터레이션 프로토콜 (Iteration protocols)
순회할 수 있는 데이터 컬렉션(배열, 문자열, 유사 배열 객체, DOM 컬렉션 등)을 만들기 위해 ES6에서 도입한 규칙입니다. ES6 이전에도 순회 가능한 데이터 컬렉션들은 통일된 규약 없이 각자 나름의 구조를 가지고 for 문, for...in 문, forEach 메서드 등 다양한 방법으로 순회할 수 있었습니다. 하지만 ES6에서 순회 가능한 데이터 컬렉션을 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for...of 문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화했습니다. 이터레이션 프로토콜은 다양한 데이터 공급자가 하나의 순회 방식을 갖도록 규정하여 효율적으로 데이터 소비자와 데이터 공급자를 연결하는 인터페이스 역할을 수행합니다.
이터러블 (Iterable)
이터러블 프로토콜을 준수한 객체를 이터러블이라 합니다. 즉, 이터러블은 Symbol.iterator
를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 객체를 말합니다. 이터러블 프로토콜은 일정 규칙만 충족한다면 어떠한 객체에 의해서도 구현될 수 있습니다. 일반 객체에서도Symbol.iterator
를 프로퍼티 키로 사용한 메서드를 구현한다면 이터러블이 된다는 의미입니다.
// 일반 객체
const obj = {}
for (const i of obj) { // -> TypeError: obj is not iterable
console.log(i)
}
// Symbol.iterator를 프로퍼티 키로 사용한 메서드를 구현
const iterable = {
[Symbol.iterator]() {},
}
for (const i of iterable) { // -> TypeError: Result of the Symbol.iterator method is not an object
console.log(i)
}
obj
의 경우 이터러블 프로토콜을 충족하지 못하기 때문에 obj is not iterable
라는 에러가 발생하지만 iterable
에서는 해당 에러가 발생하지 않습니다. 내부 Iterator가 구현되지 않아 여전히 에러를 발생시키지만 단지 Symbol.iterator
메서드를 추가하기만 하더라도 이터러블 프로토콜이 충족되었다는 것을 알 수 있습니다. 어떠한 객체가 이터러블임을 확인하려면 다음과 같은 코드를 사용하면 됩니다.
const isIterable = (v) =>
v !== null && typeof v[Symbol.iterator] === 'function';
console.log(isIterable(obj)) // false
console.log(isIterable(iterable)) // true
이터러블로 가능한 문법
for ... of 문
스프레드 문법
디스트럭처링 할당
빌트인 이터러블(built-in iterables)
빌트인으로 프로토타입 체인을 통해 Symbol.iterator
메서드를 상속받은 이터러블입니다. 예를 들어, 배열은 Array.prototype
의 Symbol.iterator
메서드를 상속받는 이터러블입니다. 이터러블은 for...of 문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있습니다.
배열
문자열
DOM 컬렉션
Map, Set
TypedArray
arguments
일반 객체의 스프레드 문법
Symbol.iterator
메서드를 직접 구현하지 않거나 상속 받지 않은 일반 객체는 이터러블이 아닙니다. 따라서 일반 객체는 for...of 문으로 순회할 수 없으며 스프레드 문법과 배열 스트럭처링 할당의 대상으로 사용할 수 없습니다. 단 현재, TC39 프로세스의 stage4(Finished) 단계에 제안되어 있는 스프레드 프로퍼티 제안은 일반 객체에 스프레드 문법의 사용을 허용합니다.
const obj = { a: 1, b: 2 }
for (const item of obj) { // -> TypeError: obj is not iterable
console.log(item)
}
console.log({ ...obj }) // { a: 1, b: 2 }
이터레이터 (Iterator)
이터레이터 프로토콜을 준수하는 객체입니다. 이터레이터는 이터러블의 요소를 탐색하기 위한 포인터 역할을 수행합니다. 이터레이터 프로토콜을 준수하기 위해서는 객체가 next()
메서드를 포함하고 next()
메서드가 value
, done
프로퍼티를 가진 리절트 객체(Result object)를 반환해야 합니다. value
는 현재 시점의 값을, done
은 순회 종료 여부를 나타냅니다. 이터레이터는 리절트 객체의 done 프로퍼티의 값이 false가 될 때까지 계속해서 이터러블의 요소를 순회하고 true가 되면 순회를 중단합니다.
// 1부터 3까지의 숫자를 반환하는 이터레이터
let i = 1
const iterator = {
next() {
return {
done: i > 3,
value: i++,
}
},
}
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: 4, done: true}
next() 메서드 규칙
arguments 없이 리절트 객체(value, done 속성 포함)를 반환해야 합니다.
done(boolean): true일 경우 순회 종료, false는 순회 유지
value(any): done이 true가 될 때까지 값 반환
유사 배열 객체를 이터러블로 만들기
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
}
for (let i = 0; i < arrayLike.length; i++) {
console.log(arrayLike[i]) // 'a', 'b', 'c'
}
for (const item of arrayLike) { // -> TypeError: arrayLike is not iterable
console.log(item)
}
유사 배열 객체는 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고 length 프로퍼티를 갖는 객체를 말합니다. 유사 배열 객체는 배열처럼 for 문으로 순회할 수 있고 인덱스로 프로퍼티 값에 접근할 수 있지만 이터러블이 아닌 일반 객체이므로 for...of 문으로는 순회가 불가합니다. 하지만 arrayLike
객체에 이터레이터를 추가하여 이터러블로 만들면 for...of 문으로 순회가 가능해집니다.
arguments, NodeList, HTMLCollection은 유사 배열 객체이면서 이터러블입니다. ES6에 이터러블이 도입되면서 해당 객체에 Symbol.iterator를 구현했기 때문입니다. 일반적으로 다른 유사 배열 객체를 배열로 변환하려면 Symbol.iterator를 직접 구현할 필요 없이 Array.from 메서드를 사용하면 됩니다.
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
// iterable protocol
[Symbol.iterator]() {
let index = -1
// iterator protocol
return {
arr: this,
next() {
index += 1
return {
value: this.arr[index],
done: index >= this.arr.length,
}
},
}
},
}
for (const item of arrayLike) {
console.log(item) // 'a', 'b', 'c'
}
const iterator = arrayLike[Symbol.iterator]()
console.log(iterator.next()) // { value: 'a', done: false }
console.log(iterator.next()) // { value: 'b', done: false }
console.log(iterator.next()) // { value: 'c', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
arrayLike
객체에 Symbol.iterator
메서드를 추가해줌으로써 이터러블 프로토콜을 만족시켰기 때문에 이터러블이 됩니다. 이로인해 for...of 문을 사용할 수 있게 되고 이터레이터를 반환해 하나씩 순차적으로 순회하는 것도 가능합니다.
이터레이터를 반환하는 함수
이터레이터는 Symbol.iterator
내부에서만 사용할 수 있는게 아니라 단독으로 사용할 수도 있습니다. 다음은 무한으로 숫자 ID를 리턴하는 이터레이터 구현 예시입니다.
function idMaker() {
let index = 0
return {
next() {
return {
value: index++,
done: false,
}
},
}
}
const iterator = idMaker()
console.log(iterator.next().value) // '0'
console.log(iterator.next().value) // '1'
console.log(iterator.next().value) // '2'
// ...
잘 정의된 이터러블 (Well-formed iterable)
이터레이터 자신이 이터러블 객체라는 것을 뜻합니다. Symbol.iterator
라는 이터레이터가 자기 자신을 리턴하기 때문에 Symbol.iterator 메서드를 호출하지 않아도 이터레이터를 반환할 수 있습니다.
{
[Symbol.iterator]() {
return this
},
next() {
return {
value: any,
done: boolean
}
}
}
피보나치를 이터러블이면서 이터레이터인 객체를 생성하여 반환하는 함수로 구현한 예제입니다.
const fibonacciFunc = (max) => {
let prev = 0
let cur = 1
// Symbol.iterator 메서드와 next 메서드를 소유한 이터러블이면서 이터레이터인 객체를 반환
return {
[Symbol.iterator]() {
return this
},
next() {
cur = prev + cur
prev = cur - prev
return {
value: cur,
done: cur >= max,
}
},
}
}
// fibonacci100은 이터러블이면서 이터레이터
const fibonacci100 = fibonacciFunc(100)
for (const i of fibonacci100) {
console.log(i) // 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}
const isWellFormed = (obj) => obj[Symbol.iterator]() === obj
console.log(isWellFormed(iterator)) // true
const iterator = fibonacciFunc(100)
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: 5, done: false }
제너레이터 (Generator)
ES6에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수입니다. 제너레이터 함수는 function* 키워드로 선언하고 하나 이상의 yield 표현식을 포함합니다. 제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해 반환합니다. 제너레이터 함수가 반환한 제너레이터 객체는 이터러블이면서 동시에 이터레이터입니다. 다시 말해, 제너레이터 객체는 Symbol.iterator 메서드를 상속받는 이터러블이면서 next 메서드를 소유하는 이터레이터입니다. 제너레이터 객체는 next 메서드를 가지는 이터레이터이므로 Symbol.iterator 메서드를 호출해서 별도로 이터레이터를 생성할 필요가 없습니다.
function* getFunc() {
yield 1
yield 2
yield 3
}
const generator = getFunc()
console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: 3, done: false }
console.log(generator.next()) // { value: undefined, done: true }
참고
이웅모. 모던 자바스크립트 Deep Dive.