어떤 Iterable은 여러 번 불러도 잘 동작하고 어떤 건 한 번만 쓸 수 있다고 하는데, 어떤 점이 그 둘을 구분하는지 알아보자.
자바스크립트의 여러 문법들(for...of, spread 등등)은 Iterable을 받는다.
그 문법들은 Iterable의 [Symbol.iterator]()를 불러서 Iterator를 받는다.
Iterable은 이터레이션이 가능한 객체를 뜻하고, 그 조건은 Iterator를 줄 수 있는지 여부다.
Iterator는 반복이 될 때 값을 돌려주는 주체이다.
우선 이터레이터는 무한으로 값을 돌려줄 수도 있고, 유한일 수도 있다.
// 무한
const infinite = {
i: 0,
next() {
return { value: this.i++, done: false };
},
};
console.log(infinite.next());
// {value: 0, done: false}
console.log(infinite.next());
// {value: 1, done: false}
console.log(infinite.next());
// {value: 2, done: false}
// ...
// 유한
const finite = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true };
},
};
console.log(finite.next());
// {value: 0, done: false}
console.log(finite.next());
// {value: 1, done: false}
console.log(finite.next());
// {value: 2, done: false}
console.log(finite.next());
// {value: undefined, done: true}
즉 끝이 날 수도 있고 안날 수도 있다.
하지만 이터레이터가 끝났다면, 그걸 다시 시작하는 건 불가능하다.
새로운 이터레이터를 돌려줘야 한다.
왜 그런지 예시로 알아보자.
const iterable = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
for (let k of iterable) {
console.log(k);
}
// 0
// 1
// 2
여기서 for...of 루프는 [Symbol.iterator]()를 불러서 Iterator(this)를 받았다.
앞서 적었듯 Iterator가 iteration을 수행하는 주체이고, 이터러블은 Iterator를 돌려줄 수 있는 객체일 뿐이다.
루프가 돌아감에 따라 i 값은 3까지 올라간다.
여기서 iterable을 다시 불러서 루프를 도는 게 가능할까?
for (let k of iterable) {
console.log(k);
}
// 출력 X
for...of 루프가 [Symbol.iterator]()를 불러서 this를 받았다.
그리고 루프가 돌기 시작하면서 next()를 부르는데, i는 이미 3이어서 {value: undefined, done: true}를 돌려주고 그대로 끝나게 된다.
따라서 정리해보면 이렇다.
next()가 값을 유한하게 돌려준다면, 위 객체의i같이 어떤 값에 반드시 의존적일 수밖에 없다.
그래서 이미 iteration이 끝난 Iterator를 다시 사용하는 건 불가능하다.
제너레이터도 마찬가지다.
function* generatorFunction() {
yield 0;
yield 1;
yield 2;
}
const iterable = generatorFunction();
for (let k of iterable) {
console.log(k);
}
// 0
// 1
// 2
for (let k of iterable) {
console.log(k);
}
// 출력 X
제너레이터 함수가 돌려주는 객체는 Generator인데(여기서 iterable), 이 객체는 Iterable이자 Iterator이다.
Generator의 next()가 돌려주는 값은 제너레이터 함수의 yield에 의해 결정되고, [Symbol.iterator]()는 자신을 돌려준다고 한다.
즉 this를 돌려준다는거다.
정리해보면 이렇다.
제너레이터의
next()도 제너레이터 함수의yield에 의존적이다.
따라서 돌려주는 값이 유한하다면, 결국 다 소모되고나면 다시 불러서 사용하는 게 불가능하다.
결론적으로 같은 이터레이터를 2번 이상 사용하는 게 불가능하다.
여러 번 쓰고 싶다면, Iterable이 불러질 때마다 다른 이터레이터를 돌려줘야한다.
즉 [Symbol.iterator]()가 매번 새로운 이터레이터를 만들어서 줘야 한다는거다.
const iterable = {
[Symbol.iterator]() {
let i = 0;
// Iterator를 만들어서 돌려줌
return {
next() {
return i < 3
? { value: i++, done: false }
: { value: undefined, done: true };
},
};
},
};
여기서 [Symbol.iterator]()는 불러질 때마다 이터레이터 객체를 만들어서 돌려준다.
for (let k of iterable) {
console.log(k);
}
// 0
// 1
// 2
for (let k of iterable) {
console.log(k);
}
// 0
// 1
// 2
따라서 Iterable을 부를 때마다 이터레이터가 다르므로 몇 번이고 다시 쓸 수 있다.
핵심은
[Symbol.iterator]()가 똑같은 이터레이터를 돌려주느냐, 아니면 매번 새 이터레이터를 만들어서 돌려주느냐다.
다시 정리를 해보자.
같은 Iterable Iterator이더라도, 이터러블이 어떤 객체를 돌려주느냐에 따라 반복 가능 여부가 달라진다.
// 한 번만 사용 가능
const iterableIterator = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true };
},
// 같은 객체를 돌려줌
[Symbol.iterator]() {
return this;
},
};
for (let k of iterableIterator) {
console.log(k);
}
// 0
// 1
// 2
for (let k of iterableIterator) {
console.log(k);
}
// 출력 X
this로 동일한 객체를 매 번 돌려주기 때문에, 재사용이 불가능하다.
// 여러 번 사용 가능
const iterableIterator = {
// 매 번 객체를 새로 만들어서 돌려줌
[Symbol.iterator]() {
let i = 0;
return {
next() {
return i < 3
? { value: i++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (let k of iterableIterator) {
console.log(k);
}
// 0
// 1
// 2
for (let k of iterableIterator) {
console.log(k);
}
// 0
// 1
// 2
매번 새 객체를 만들어서 돌려주기 때문에 재사용이 가능하다.
그리고 제너레이터는 한 번만 쓸 수 있지만, 매 번 제너레이터 함수를 불러주면 여러 번 쓸 수 있다 :
function* generatorFunction() {
yield 0;
yield 1;
yield 2;
}
const generator = generatorFunction();
for (let k of generator) {
console.log(k);
}
// 0
// 1
// 2
for (let k of generator) {
console.log(k);
}
// 출력 X
하나의 제너레이터는 반복이 끝나면 다시 사용할 수 없다.
function* generatorFunction() {
yield 0;
yield 1;
yield 2;
}
for (let k of generatorFunction()) {
console.log(k);
}
// 0
// 1
// 2
for (let k of generatorFunction()) {
console.log(k);
}
// 0
// 1
// 2
매 번 제너레이터 함수를 불러서 제너레이터를 생성하면, 그때마다 이처럼 사용이 가능하다.
제너레이터가 자기 자신(this)을 돌려주는 이터레이터라는 사실은 변하지 않는다.
하지만 매 번 generatorFunction()을 써주면 그 이터레이터는 딱 한 번만 쓰이고 끝난다.
부를 때마다 새 제너레이터를 만들어서 주기 때문이다.
일반적으로 제너레이터 함수와 제너레이터를 통해서만 Iterator나 Iterable을 만들고 쓸 것이다. 그러면 그걸 어떻게 구성해야할까?
핵심은 결국 이터러블 메서드인 [Symbol.iterator]()가 새로운 이터레이터를 만들어서 돌려줘야하는거다.
그리고 제너레이터 함수는 불러질 때마다 새로운 제너레이터를 만들어서 준다.
따라서 이걸 합쳐서
[Symbol.iterator]에제너레이터 함수를 값으로 준다면 여러 번 쓸 수 있는 이터러블이 완성된다.
어쨌든 이터러블 메서드가 매 번 Iterator 객체를 만들며 돌려주면 되므로, 직접 Iterator 객체를 만들면서 돌려줄 수 있을 것이다 :
const iterable = {
[Symbol.iterator]: function () {
let i = 0;
// Iterator 객체를 만들어서 돌려준다
return {
next() {
return i < 3
? { value: i++, done: false }
: { value: undefined, done: true };
},
};
},
};
이렇게 구현해도 여러 번 사용 가능하지만 작성하기 상대적으로 복잡하다.
const iterable = {
// Generator를 만들어서 돌려주는 제너레이터 함수
[Symbol.iterator]: function* () {
for (let i = 0; i < 3; i++) {
yield i;
}
},
};
이터러블 메서드에 제너레이터 함수를 써주면 이터레이터가 돌려줄 값을 더 간편하고 쉽게 적을 수 있다.
for (let k of iterable) {
console.log(k);
}
// 0
// 1
// 2
iterable의 [Symbol.iterator]()(제너레이터 함수)를 부르고 제너레이터를 받았다.
그리고 이 제너레이터의 next()를 부르면서 각 값을 얻었다.
여기서 제너레이터의 이터러블 메서드인 [Symbol.iterator]()는 쓰이지 않는다.
즉, 제너레이터는 Iterator로서의 역할만 하며, next() 메서드만이 쓰인다.
const iterable = {
*[Symbol.iterator]() {
for (let i = 0; i < 3; i++) {
yield i;
}
},
};
method definition으로 줄여서 적으면 이렇게 된다.
그리고 이 메서드 하나만 구현하면 어떤 객체든 간에 자신의 프로퍼티를 돌면서 yield 해주면 바로 iterable하게 되기 때문에 유용하다고 한다.
수정 : 2025-01-24