-
ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 순회 가능한(iterable) 데이터 컬렉션을 만들기 위해 ECMAScript 사양에 정의하여 미리 약속한 규칙이다.
-
EC6 이전의 순회 가능한 데이터 컬렉션(array, string, array-like, DOM collection 등)은 통일된 규약(protocol)없이 각자 나름의 구조를 가지고
for
문,for…in
문,forEach
메서드 등 다양한 방법으로 순회할 수 있었다. -
순회 가능한 데이터 컬렉션을 이터레이션 프로토콜을 준수하는 이터러블로 통일하였다.
-
⭐️ 이터러블은
for…of
문, 스프레드 문법, 디스트럭처링의 대상으로 사용 가능하다. -
이터러블 프로토콜(iterable protocol)
-
이터레이터 프로토콜(iterator protocol)
-
이터러블 객체 = 이터러블 프로토콜을 준수한 객체
-
어떤 객체가 이터러블인지 확인하는 방법:
Symbol.iterator
프로퍼티 키를 가지고 있는지 확인한다const isIterable = (v) => v !== null && typeof v[Symbol.iterator] === 'function'; isIterable([]); // true isIterable(''); // true isIterable(new Set()); // true isIterable(new Map()); // true isIterable({}); // false Symbol.iterator in []; // true Symbol.iterator in {}; // false const obj = { a: 1, b: 2 }; for (const item of obj) { } // TypeError: obj is not iterable // 일반 객체는 iterable하지 않지만 // 객체 리터럴 내부에서 스프레드 문법을 허용한다. const clonedObj = { ...obj };
-
이터러블의
Symbol.iterator
을 호출하면 이터레이터 프로토콜을 준수한 이터레이터가 반환된다. -
반환된 이터레이터는
next
메서드를 갖는다.const iterator = [][Symbol.iterator](); console.log('next' in iterator); // true
-
next
메서드는 이터러블의 각 요소를 순회하기 위한 포인터 역할을 한다. -
즉,
next
메서드를 호출할 때마다 이터러블을 순회하며 순회 결과로 이터레이터 리절트 객체(iterator result object)를 반환한다.const array = [1, 2, 3]; const iterator = array[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} // 문자열은 이터러블이다. const string = 'hi'; // Symbol.iterator 메소드는 이터레이터를 반환한다. const stringIter = string[Symbol.iterator](); // 이터레이터는 next 메소드를 소유한다. // next 메소드는 이터레이터 리절트 객체를 반환한다. console.log(stringIter.next()); // {value: "h", done: false} console.log(stringIter.next()); // {value: "i", done: false} console.log(stringIter.next()); // {value: undefined, done: true} // 이터러블은 for...of 문으로 순회 가능하다. for (const letter of string) { console.log(letter); }
- 자바스크립트는 이터레이션 프로토콜을 준수하는 객체인 빌트인 이터러블을 제공한다.
빌트인 이터러블 | Symbol.iterator 메서드 |
---|---|
Array | Array.prototype[Symbol.iterator] |
String | String.prototype[Symbol.iterator] |
Map | Map.prototype[Symbol.iterator] |
Set | Set.prototype[Symbol.iterator] |
TypedArray | TypedArray.prototype[Symbol.iterator] |
arguments | arguments[Symbol.iterator] |
DOM 컬렉션 | NodeList.prototype[Symbol.iterator] |
HTMLCollection.prototype[Symbol.iterator] |
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator
for … of
문은 이터러블을 순회하면서 이터러블 요소를 변수에 할당한다.
for(변수선언문 of 이터러블) { ... }
for … in
문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트[[Enumerable]]
의 값이true
인 프로퍼티를 순회하며 열거(enumeration)한다. 프로퍼티키가 symbol인 프로퍼티는 열거하지 않는다.
for(변서선언문 in 객체) { ... }
const obj = { a: 1, b: 10 };
for (const item in obj) {
console.log(item);
} // a b
-
for … of
문 내부적으로 이터레이터의next
메서드를 호출하여 이터러블을 순회한다.next
메서드가 반환하는 이터레이터 리절트 객체의value
값을 변수에 할당한다.done
값이false
이면 순회하고true
이면 순회를 중단한다.
const array = [1, 2, 3]; // item에 이터레이터 리절트 객체의 value 값이 할당된다. for (const item of array) { // 이터레이터 리절트 객체의 done 값이 true이면 순회를 중단한다. console.log(item); // 1 2 3 }
-
유사 배열 객체: 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고
length
프로퍼티를 갖는 객체- 아래 예제의
arrayLike
와 같이 유사 배열 객체는 이터러블이 아니기 때문에for … of
문을 사용할 수 없다.
const arrayLike = { 0: 10, 1: 20, 2: 30, length: 3, }; for (const item of arrayLike) { console.log(item); } // TypeError: arrayLike is not iterable
- 아래 예제의
-
Array.from
메서드를 사용해 배열로 간단하게 변환할 수 있다. (⭐️정말 유용함)- “
Array.from()
메서드는 **유사 배열 객체(array-like object)나 순회 가능한 객체(iterable object)**를 얕게 복사해 새로운Array
객체를 만듭니다.” - https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from
console.log(Array.from(arrayLike)); // [10, 20, 30]
Array.from
사용 예시
// Array(1024).keys()는 iterator이기 때문에 // map과 같은 배열 메서드를 사용할 수 없다. // Array.from을 통해 iterator 객체를 배열로 만들 수 있다. // 그러면 배열 메서드를 사용할 수 있게 된다. const users = Array.from(Array(1024).keys()).map( (id): User => ({ id, name: `denis${id}`, }) );
- “
이터레이션 프로토콜을 준수하는 이터러블
Array, String, Map, Set, TypedArray(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array), DOM data structure(NodeList, HTMLCollection), Arguments
- 다양한 **데이터 공급자(Data provider)**가 각자의 순회 방식을 갖는다면 **데이터 소비자(Data consumer)**는 다양한 데이터 소스의 순회 방식을 모두 지원해야 한다.
- 이는 효율적이지 않다. 하지만 다양한 데이터 공급자가 이터레이션 프로토콜을 준수하도록 규정하면 데이터 소비자는 이터레이션 프로토콜만을 지원하도록 구현하면 된다.
- 이터레이션 프로토콜은 데이터 소비자와 데이터 공급자를 연결하는 인터페이스의 역할을 한다.
- 주석을 하나씩 보면서 이터러블 객체를 어떻게 구현하는지 확인할 수 있다.
- 이터러블 객체는
for … of
문, 스프레드 문법, 디스트럭처링 할당에서 사용할 수 있다.
let range = {
from: 1,
to: 10,
};
// Symbol.iterator 메서드를 구현하여 이터러블 프로토콜을 준수한다.
range[Symbol.iterator] = function () {
let [current, last] = [this.from, this.to];
// Symbol.iterator 메서드는 next 메서드를 소유한 이터레이터를 반환한다.
// next 메서드는 이터레이터 리절트 객체를 반환한다.
return {
next() {
if (current <= last) {
return { done: false, value: current++ };
} else {
return { done: true };
}
},
};
};
// for ... of 문에서 이터러블을 순회할 수 있다.
// 이터러블인 range 객체를 순회할 때마다 next 메서드가 호출된다.
for (let num of range) {
console.log(num);
}
// 스프레드 문법 가능
const arr = [...range];
console.log(arr);
// 디스트럭처링 할당 가능
const [first, second, ...rest] = range;
console.log(first, second, rest);
- 위 예제를 보면
range
객체의from
과to
프로퍼티 값이 1과 10으로 각각 고정되어 있다. range
의from
과to
값을 외부에서 전달받아 반환하는 함수를 만들면 된다.
const range = function (from, to) {
let [current, last] = [from, to];
// 이터러블을 반환한다.
return {
[Symbol.iterator]() {
return {
next() {
if (current <= last) {
return { done: false, value: current++ };
} else {
return { done: true };
}
},
};
},
};
};
for (let num of range(3, 10)) {
console.log(num);
}
- 앞서 살펴본 range 함수는 이터러블을 반환한다.
- 이터레이터를 생성하려면 이터러블의 Symbol.iterator 메서드를 호출해야 한다.
const iterable = range(3, 10);
// 이터레이터를 얻기 위해 이터러블의 Symbol.iterator 메서드를 호출한다.
const iterator = iterable[Symbol.iterator]();
console.log(iterator.next()); // {done: false, value: 3}
console.log(iterator.next()); // {done: false, value: 4}
console.log(iterator.next()); // {done: false, value: 5}
console.log(iterator.next()); // {done: false, value: 6}
- 이터러블이면서 이터레이터인 객체를 생성하면
Symbol.iterator
메서드를 호출하지 않아도 된다.
const range = function (from, to) {
let [current, last] = [from, to];
// Symbol.iterator 메서드와 next 메서드를 소유한
// 이터러블이면서 이터레이터인 객체 반환
return {
[Symbol.iterator]() {
return this;
},
// next 메서드는 이터레이터 리절트 객체 반환
next() {
if (current <= last) {
return { done: false, value: current++ };
} else {
return { done: true };
}
},
};
};
for (let num of range(3, 10)) {
console.log(num);
}
// range 함수는 이터러블이면서 이터레이터인 객체를 생성한다.
let iter = range(3, 10);
console.log(iter.next()); // {done: false, value: 3}
console.log(iter.next()); // {done: false, value: 4}
console.log(iter.next()); // {done: false, value: 5}
console.log(iter.next()); // {done: false, value: 6}
- 무한 수열 구현 가능
const range = function () {
let current = 1;
// 이터러블을 반환한다.
return {
[Symbol.iterator]() {
return {
next() {
return { value: current++ };
},
};
},
};
};
for (let num of range()) {
if (num > 100) break;
console.log(num); // 1 2 3 ... 99 100
}
const [a1, a2, a3] = range();
console.log(a1, a2, a3); // 1 2 3
- 이터러블은 데이터 공급자의 역할을 한다.
- 배열이나 문자열 등은 모든 데이터를 메모리에 미리 확보한 다음 데이터를 공급한다.
- 하지만 이터러블은 **지연 평가(lazy evaluation)**을 통해 데이터를 생성한다.
- 즉, 필요한 데이터를 필요한 순간에 생성하므로 빠른 실행 속도를 기대할 수 있고, 불필요한 메모리 소비를 하지 않는다.