[JS] 모던 Javascript Deep Dive 34장 - 이터러블
업데이트:
이터러블이라는 용어가 생소해서 와닿지 않는데 책의 첫부분부터 이터러블 프로토콜을 설명해서 이해하기가 어려웠다 😭
자세히 공부하기 앞서 이터러블은 쉽게 말해 반복 가능한 객체라고 미리 알아두고 가면 좋을 것 같다.
그리고 Symbol 장에서 Symbol.iterator 메서드를 배웠는데 자세히 다루지 않아서 다시 복습하고 넘어가보려고 한다.
Symbol.iterator
일반 객체에 Symbol.iterator 메서드를 추가해 이터러블처럼 동작하도록 구현하려고 한다.
let range = {
from: 1,
to: 5
};
// 위 객체를 for...of 문을 사용하여 1부터 5까지 반복하는 동작을 만드는 것이 목표이다.
// 1. Symbol.iterator 메서드 추가하기
range[Symbol.iterator] = function() {
return {
current: this.from,
last: this.to,
// 2. for ..of 반복문에 의해 반복마다 next()가 호출된다.
next(){
if (this.current <= this.last) {
return { done: false, value: this.current++ }; // 3. next()는 값을 객체 {done: Boolean, value : any} 형태로 반환해야 한다.
} else {
return { done: true };
}
}
};
};
for (let num of range){
console.log(num); // 1 2 3 4 5
}
원래 일반 객체에는 next()
가 없다.
대신 range[Symbol.iterator]()
을 호출해서 만든 ‘이터레이터’ 객체와 이 객체의 메서드 next()에서 반복에 사용될 값을 만들어 낸 것이다.
[01] 이터러블 프로토콜
이터레이션 프로토콜은 데이터 컬렉션을 순회하기 위한 프로토콜으로 ES6에서 도입되었다.
여기서 프로토콜은 규약/규칙이고 데이터 컬렉션은 자료구조이다.
그렇다면 순회 가능한 데이터 컬렉션(자료구조)은 무엇이 있을까?
대표적으로 배열, 문자열, 유사 배열 객체, DOM 컬렉션 등이다. 모두 for문으로 순회가능한 자료구조이다.
ES6부터는 이들을 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for…of문, 뒷장에서 배울 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화했다.
이터레이션 프로토콜에는 이터러블 프로토콜과 이터레이터 프로토콜이 있는데 말이 너무 비슷해서 헷갈리니 차차 코드를 보면서 개념을 정리해보자 !
1) 이터러블
이터러블 프로토콜을 준수한 객체를 이터러블이라 한다.
🎯 이터러블 예
- 배열 ( [ ] )
- 문자열 ( ‘’ )
- Map (new Map())
- Set (new Set())
그렇다면 일반 객체는 이터러블이 될 수 있을까?
Symbol.iterator 메서드를 직접 구현하거나 상속받고 있지 않기 때문에 이터러블 프로토콜을 준수한 이터러블이 아니다.
일반 객체는 for … of문으로 순회할 수도 없고 스프레드 문법 / 배열 디스트럭처링 할당의 대상으로도 사용할 수 없다.
2) 이터레이터
Symbol.iterator 메서드가 반환한 이터레이터는 next 메서드를 갖는다.
const arr = [1, 2, 3]; // 배열은 이터러블이다.
const iterator = array[Symbol.iterator](); // Symbol.iterator 메서드는 이터레이터를 반환
console.log('next' in iterator); // true
//next 메서드를 가짐
이터레이터의 next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터의 역할을 한다.
next 메서드는 이터러블을 순차적으로 순회하고 그 결과를 나타내는 이터레이터 리절트 객체를 반환한다.
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: 4, done: true }
[02] 빌트인 이터러블
[03] for … of 문
for … of 문은 for … in 문과 매우 유사하다.
const obj = { 1: one, 2: two, 3: three };
for(const x in obj){
console.log(x); // 1 2 3
}
for(const x of obj){
console.log(x);
} // Uncaught TypeError: obj is **not iterable** at <anonymous>:1:16
for (const item of [1,2,3]){
console.log(item); // 1 2 3
}
for …in 문은 객체의 프로퍼티를 순회하며 열거하고 for …of 문은 이터러블을 순회하여 이터레이터 리절트 객체의 프로퍼티 값을 변수에 할당한다.
[04] 이터러블과 유사 배열 객체
유사 배열 객체는 순회가 가능하고 인덱스에 접근할 수도 있지만 이터러블이 아닌 일반 객체다.
Symbol.iterator 메서드가 없기 때문에 for… of 문으로도 순회할 수 없다.
그런데 ES6부터는 이터러블이 도입되면서 arguments, NodeList, HTMLCollection 등 여러 유사 배열 객체가 이터러블이 되었다.
🔍 유사 배열 객체이면서 이터러블이 아닌 경우
일반 객체에 인덱스 번호와 length 프로퍼티를 지정해주어 유사 배열 객체로 만든 코드다.
const arrayLike = {
0: 1,
1: 2,
2: 3,
length: 3
};
const arr = Array.from(arrayLike); // Array.from은 유사 배열 객체를 배열로 변환한다.
console.log(arr); // [1, 2, 3]
[05] 이터레이션 프로토콜의 필요성
이제 이터레이션을 대강 알았으니 이터레이션 프로토콜을 다시 정리해보자
- 이터러블 프로토콜 : Symbol.iterator을 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환하는 규약
- 이터레이터 프로토콜 : next 메서드를 호출하면 이터러블을 순회하며 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 규약
이러한 이터레이션 프로토콜은 배열, 문자열, Map/Set 과 같은 데이터 공급자가 하나의 순회 방식을 갖도록 규정하여 데이터 소비자가 효율적으로 사용할 수 있도록 데이터 소비자와 데이터 공급자를 연결하는 인터페이스의 역할을 한다.
[06] 사용자 정의 이터러블
1) 사용자 정의 이터러블 구현
일반 객체를 이터레이션 프로토콜을 준수하도록 하여 피보나치 수열을 이터러블로 구현할 수 있다.
const fibonacci = {
// Symbol.iterator 메소드를 구현하여 이터러블 프로토콜을 준수
[Symbol.iterator]() {
let [pre, cur] = [0, 1];
// 최대값
const max = 10;
// Symbol.iterator 메소드는 next 메소드를 소유한 이터레이터를 반환해야 한다.
// next 메소드는 이터레이터 리절트 객체를 반환
return {
// fibonacci 객체를 순회할 때마다 next 메소드가 호출된다.
next() {
[pre, cur] = [cur, pre + cur];
return {
value: cur,
done: cur >= max
};
}
};
}
};
// 이터러블의 최대값을 외부에서 전달할 수 없다.
for (const num of fibonacci) {
// for...of 내부에서 break는 가능하다.
// if (num >= 10) break;
console.log(num); // 1 2 3 5 8
}
2) 이터러블을 생성하는 함수
이전 피보나치 수열 코드는 최댓값이 항상 정해져 있어 외부에서 값을 전달해 줄 수는 없다.
이터러블을 반환하면 수열의 최댓값을 외부에서 전달하는 피보나치 수열 함수도 만들 수 있다.
const fibonacciFunc = function (max) {
let [pre, cur] = [0, 1];
// 이터러블을 반환문에 넣어줌
return {
[Symbol.iterator]() {
return {
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: cur >= max };
}
}
}
}
};
for (const num of fibonacciFunc(10)) {
console.log(num); // 1 2 3 5 8
}
3) 이터러블이면서 이터레이터인 객체를 생성하는 함수
// 이터러블이면서 이터레이터인 객체를 반환하는 함수
const fibonacciFunc = function (max) {
let [pre, cur] = [0, 1];
// Symbol.iterator 메소드와 next 메소드를 소유한
// 이터러블이면서 이터레이터인 객체를 반환
return {
// Symbol.iterator 메소드
[Symbol.iterator]() {
return this;
},
// next 메소드는 이터레이터 리절트 객체를 반환
next() {
[pre, cur] = [cur, pre + cur];
return {
value: cur,
done: cur >= max
};
}
};
};
// iter는 이터러블이면서 이터레이터이다.
let iter = fibonacciFunc(10);
// iter는 이터레이터이다.
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: 5, done: false}
console.log(iter.next()); // {value: 8, done: false}
console.log(iter.next()); // {value: 13, done: true}
iter = fibonacciFunc(10);
// iter는 이터러블이다.
for (const num of iter) {
console.log(num); // 1 2 3 5 8
}
4) **무한 이터러블과 Lazy evaluation(지연 평가)**
// 무한 이터러블을 생성하는 함수
const fibonacciFunc = function () {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return this;
},
next() {
[pre, cur] = [cur, pre + cur];
// done 프로퍼티를 생략한다.
return { value: cur };
}
};
};
// fibonacciFunc 함수는 무한 이터러블을 생성한다.
for (const num of fibonacciFunc()) {
if (num > 10000) break;
console.log(num); // 1 2 3 5 8...
}
// 무한 이터러블에서 3개만을 취득한다.
const [f1, f2, f3] = fibonacciFunc();
console.log(f1, f2, f3); // 1 2 3
fibonacciFunc()
함수는 무한 이터러블을 생성한다. next()
메서드 return 문에 done 값이 없기 때문이다.
그런데 이 무한 이터러블 피보나치 함수는 for …of 문에서 할당이 실행되기 전까지 데이터를 생성하지는 않는다.
즉, 데이터가 필요할 때까지 데이터의 생성을 지연하다가 데이터가 필요한 순간 데이터를 생성한다고 한다.
이 무한 이터러블을 잘 활용하면 효율적인 프로그래밍을 할 수 있을 것 같다.
댓글남기기