앞에서 이터러블을 알아볼때 [Symbol.iterator] 메서드를 활용해서 구현했던게 기억나시나요? 한번 더 복습해봅시다.
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ }
} else {
return { done: true }
}
}
}
}
};
for (let value of range) {
console.log(value);
}
결국 핵심은 for ... of
문이 처음 실행될 때 [Symbol.iterator]
가 실행되고 각각의 반복에 대해서 next
메서드가 호출됩니다.
다음으로 만약 비동기를 다루는 이터러블 객체를 구현하는 방법에 대해 알아보겠습니다.
비동기 이터러블 객체를 구현하려면 기존 이터러블 객체 구현 방법에서 세가지를 바꿔주면 됩니다.
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
async next() {
await new Promise((resolve) => setTimeout(resolve, 2000));
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
};
(async () => {
for await (let value of range) {
console.log(value);
}
})();
네. 제네레이터 함수를 실행해서 얻은 객체도 이터러블 객체이기 때문에 for...of로 순회할 수 있습니다. 그럼 이터러블 객체가 되기 위한 조건이 뭘까요? 그걸 알면 되겠죠.
[Symbol.iterator]
메서드를 가지고 있어야합니다.[Symbol.iterator]
메서드에 의해 반환되는 객체는 반드시 next
메서드를 가지고 있어야 합니다.제네레이터 함수에 의해서 반환되는 객체는 이 두가지 조건을 만족하므로 이터러블한 객체입니다.
그럼 제네레이터 함수를 활용해서 이터러블 객체를 구현하려면 어떻게 해야 할까요? 간단합니다. [Symbol.iterator] 앞에 제네레이터 함수를 만드는 *를 붙여주면 됩니다.
즉, for...of
문이 호출될때 처음으로 제네레이터 객체를 반환할 것이고 이 제네레이터 객체는 next
메서드를 가지고 있으므로 각각의 이터레이션마다 next
를 호출해 i
를 yield
할 것입니다.
const range = {
from: 1,
to: 5,
*[Symbol.iterator]() {
for (let i = this.from; i <= this.to; i++) {
yield i;
}
},
};
for (let value of range) {
console.log(value);
}
그럼 여기에 비동기 동작을 추가하려면 어떻게 해야 할까요? 다음 섹션에서 살펴보도록 합시다.
간단합니다. async 키워드를 사용하면 됩니다. 바로 코드를 살펴보겠습니다.
const range = {
from: 1,
to: 5,
async *[Symbol.iterator]() { // 여기에 말이죠.
for (let i = this.from; i <= this.to; i++) {
// 이렇게 await를 사용할 수도 있습니다.
await new Promise(resolve => setTimeout(resolve, 1000))
yield i;
}
},
};
for (let value of range) {
console.log(value); // 1초 후... 1, 1초 후... 2, 1초 후... 3, 1초 후... 4, 1초 후... 5
}
그럼 이 비동기 제네레이터를 실무에서 어떻게 활용할 수 있을까요? 대표적으로 pagenation에 활용될 수 있습니다. 기준으로 잡은 숫자만큼의 데이터를 불러오는 식이죠. 아래의 코드를 확인해보죠.
async function* fetchUsers() {
let url = "https://jsonplaceholder.typicode.com/users/1";
while (url) {
const response = await fetch(url);
const data = await response.json();
const userId = url.split("/")[url.split("/").length - 1];
url = url.replace(/[0-9]+$/, ++userId);
yield data;
}
}
(async () => {
for await (let user of fetchUsers()) {
// 매크로태스크 큐를 활용해서 이벤트를 block시키지 않도록 합니다.
setTimeout(() => console.log(user));
}
})();
그럼 아래의 사진처럼 순차적으로 데이터를 콘솔에 출력합니다.
비동기 제네레이터 그리고 이터러블 객체와 어떤 관련이 있는지 알아보고, 유스케이스까지 직접 구현해봤습니다. 프런트에서 redux-saga 미들웨어를 사용할때 제네레이터가 사용된것 말고 평소에 유스케이스에 대해서는 몰랐는데 직접 데이터를 처리하면서 이해도가 상승한것 같네요.