💧

[JS] Iterable(이터러블)


Iterable, 한국어로 '반복 가능한'이라는 뜻을 가진 객체는 배열을 일반화한 객체다. 이 개념을 사용하면 어느 객체에든 for..of 문을 사용할 수 있다.

배열과 문자열이 대표적인 iterable이다.

Symbol.iterable

어떤 객체를 iterable로 만들기 위해서는 객체에 Symbol.iterator라는 메서드를 추가해 아래와 같은 일이 벌어지게 해야한다.

  1. for..of를 호출하면, for..ofSymbol.iterator를 호출한다(없으면 에러발생). Symbol.iterator는 반드시 iterator(메서드 next()가 있는 객체)를 반환해야 한다.

  2. for..of는 반환된 iterator를 대상으로 작동한다.

  3. for..of에서 다음 값이 필요하면, for..of는 iterator의 next()를 호출한다.

  4. next()의 반환값은 { done: Boolean, value: any }의 형태를 가져야한다. done은 반복이 끝났는지 여부를 알려준다. donefalsevalue에 다음 값이 저장된다.

1// 1번 방식
2let range = {
3 from: 1,
4 to: 5,
5};
6range[Symbol.iterator] = function () {
7 return {
8 current: this.from,
9 last: this.to,
10 next() {
11 if(this.current <= this.last) return { done: false, value: this.current++ };
12 else return { done: true };
13 }
14 }
15}
16// 2번 방식
17let range = {
18 from: 1,
19 to: 5,
20 [Symbol.iterator]() {
21 this.current = this.from;
22 return this;
23 },
24 next() {
25 if(this.current <= this.to) return { done: false, value: this.current++ };
26 else return { done: true };
27 }
28}
29
30for (const num of range) {
31 console.log(num);
32}
33// -> 1, 2, 3, 4, 5

1번 방식과 2번 방식의 차이점은 '관심사의 분리(Seperate of Concern, SoC)'다.

1번 방식에서는 range가 내부적으로 메서드 next()를 가지지 않는다. 대신 range[Symbol.iterator]()를 호출해서 만든 iterator 객체와 이 객체의 next()에서 반복에 사용될 값을 만들어낸다. 이렇게 함으로써 iterator 객체와 대상 객체를 분리할 수 있다.

2번 방식은 range[Symbol.iterator]()가 대상 객체를 반환한다. 반환된 객체에는 next()도 있고 this.current에 반복이 얼마나 진행되었는지 나타내는 값도 저장되어 있다. range 자체가 iterator가 된 것이다.

Iterator는 명시적으로 호출할 수도 있다.

1let str = "Hello";
2let iterator = str[Symbol.iterator]();
3while(true) {
4 let result = iterator.next();
5 if(result.done) break;
6 console.log(result.value);
7}
8// -> H, e, l, l, o

async 이터레이터

비동기 이터레이터를 사용하면 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있다. 네트워크를 통해 데이터가 여러 번에 걸쳐 들어오는 상황을 처리할 때 유용하게 사용할 수 있다.

비동기 이터레이터는 Symbol.iterator 대신, Symbol.asyncIterator를 사용해야 한다. 그리고 next()는 프로미스를 반환해야 한다.

1let range = {
2 from: 1,
3 to: 5,
4 [Symbol.asyncIterator]() {
5 return {
6 current: this.from,
7 last: this.to,
8 async next() {
9 await new Promise(resolve => setTimeout(resolve, 1000));
10
11 if(this.current <= this.last) return { done: false, value: this.current++ };
12 else return { done: true };
13 }
14 }
15 }
16}
17(async () => {
18 for await (let value of range) {
19 console.log(value);
20 }
21})();
22// -> 1, 2, 3, 4, 5 가 1초 간격으로 출력

Iterable과 유사 배열

  • IterableSymbol.iterator 메서드가 구현된 객체다.

  • 유사 배열(Array-like)는 인덱스와 length 프로퍼티가 있어 배열처럼 보이는 객체다.

1let arrayLike = {
2 0: "안",
3 1: "녕",
4 length: 2
5};
6
7for (const item of arrayLike) {}
8// -> 에러 발생

문자열은 iterable이면서 유사 배열인 객체다.

Iterable과 유사배열을 배열처럼, 즉 배열 메서드를 사용하고 싶을 때 Array.from을 사용할 수 있다.

Array.from

Array.from은 iterable이나 유사 배열을 받아 '진짜' 배열을 만들어준다. 이 과정을 통해 iterable이나 유사 배열에 배열 메서드를 사용할 수 있다.

1let arrayLike = {
2 0: "안",
3 1: "녕",
4 length: 2
5};
6
7let array = Array.from(arrayLike);
8console.log(array.pop());
9// -> "녕"

7번 줄의 Array.from은 객체를 받아 iterable이나 유사 배열인지 검사한다. 넘겨 받은 인수가 iterable이나 유사 배열인 경우, 새로운 배열을 만들고 객체의 모든 요소를 새롭게 만든 배열로 복사한다.

Array.from에는 매핑 함수를 선택적으로 넘겨줄 수 있다. arr.map()과 비슷하게 요소를 추가하기 전에 각 요소를 대상으로 매핑을 해준다.

1let array = Array.from(range, num => num * num);
2console.log(array);
3// -> [1, 4, 9, 16, 25]

Array.fromstr.split과 다르게 문자열 자체가 가진 iterable 프로퍼티를 이용해 동작한다. 따라서 for..of처럼 서로게이트 쌍에도 제대로 적용된다.

1let str = '𝒳😂';
2
3console.log(Array.from(str));
4// -> ["𝒳", "😂"]
5console.log(str.split(''));
6// -> ["�", "�", "�", "�"]

이것을 이용해서 서로게이트 쌍을 처리할 수 있는 slice를 직접 구현할 수도 있다.

1function slice(str, start, end) {
2 return Array.from(str).slice(start, end).join('');
3}

참조: ko.javascript.info