일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Headless 컴포넌트
- Custom Hook
- Microtask Queue
- helm-chart
- 주니어개발자
- docker
- linux 배포판
- jotai
- TypeScript
- zustand
- Recoil
- 암묵적 타입 변환
- CS
- 클라이언트 상태 관리 라이브러리
- Render Queue
- task queue
- react
- 타입 단언
- JavaScript
- 명시적 타입 변환
- type assertion
- Compound Component
- 좋은 PR
- AJIT
- 프로세스
- prettier-plugin-tailwindcss
- useLayoutEffect
- useCallback
- Redux Toolkit
- Sparkplug
- Today
- Total
구리
[JavaScript] Iterable, Iterator 그리고 Generator (2) - async 이터레이터와 제너레이터 본문
[JavaScript] Iterable, Iterator 그리고 Generator (2) - async 이터레이터와 제너레이터
guriguriguri 2023. 7. 30. 22:40이터레이터는 동기식 데이터 소스를 나타내는데 적합합니다. 하지만 I/O 접근이 필요한 데이터는 일반적으로 이벤트 기반이거나 스트리밍 비동기 API를 사용하기에 이터레이터를 사용할 수 없습니다.
이런 비동기 데이터 소스에 대한 데이터 접근 프로토콜을 제공하기 위해 비동기 이터레이션이 추가되었습니다.
ES9(ES2018)에서는 비동기 이터레이션을 지원하는 async 이터레이터, for...await of
문이 도입되었으며 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있습니다. 또한 비동기 제너레이터를 사용하면 이런 데이터를 더 편리하게 처리할 수 있습니다.
async 이터레이터
비동기 이터레이터는 next()
가 { value, done }
객체를 위한 프라미스 (Promise)를 반환하는 것을 제외하면 기존의 동기식 이터레이터와 유사합니다.
비동기 이테레이터의 내부 설계에는 요청 큐의 설계가 포함되어 있습니다. 이전 요청의 결과가 해결되기 전에 이터레이터 메서드가 여러번 호출될 수 있기에, 모든 이전 요청 조작이 완료될 때까지 각 메서드 호출을 내부적으로 큐에 넣어야 합니다.
이터러블 객체를 비동기적으로 만들기 위해선 다음 작업이 필요합니다.
- 이터러블 객체에 필요한
Symbol.iterator
대신Symbol.asyncIterator
를 사용 next()
는 프라미스를 반환- 비동기 이터러블 객체를 반복 순회하기 위해선
for...await of
문을 사용
참고로 스프레드 문법은 비동기적으로 동작하지 않기에 비동기 이터레이터와 사용할 수 없으며 일반 이터레이터만 가능합니다.
아래는 1부터 5까지 숫자를 비동기적로 반환하는 이터러블 객체 range의 예시 코드입니다.
let range = {
from: 1,
to: 5,
// for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다.
[Symbol.asyncIterator]() { // (1)
// Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다.
// 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
// 다음 값은 next()에서 정해집니다.
return {
current: this.from,
last: this.to,
// for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
async next() { // (2)
// next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
// (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)
// 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다.
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()
위 예시의 next() 처럼 async 메서드일 필요는 없으며, 프라미스를 반환하는 일반 메서드여도 괜찮지만 async를 사용하면 await도 사용할 수 있기에 편의상 사용했습니다.
async 제너레이터
일반 제너레이터에선 await
을 사용할 수 없는 동기적 문법으로 모든 값은 동기적으로 생산됩니다.
만약 네트워크 요청과 같이 제너레이터에 await
을 사용해야 한다면 async
를 제너레이터 함수 앞에 붙여주면 됩니다.
일반 제너레이터와 async
제너레이터 함수의 차이점은 다음과 같습니다.
async
제너레이터 함수를 통해 생성된 제너레이터 객체의next()
는 비동기적으로 처리next()
는 프라미스를 반환하기에await
혹은then()
을 사용해 값에 접근
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// await를 사용할 수 있습니다!
await new Promise((resolve) => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
console.log(value); // 1, 2, 3, 4, 5
}
})();
위에서 작성한 async
이터러블 객체는 async
제너레이터 함수를 이용해 더 간단히 구현할 수 있습니다.
이터러블 객체를 만들 경우 Symbol.iterator
에 next
가 구현된 객체를 반환하기보단 제너레이터를 반환하도록 구현하는 경우가 많기에 async
이터러블 객체를 만들 때도 Symbol.asyncIterator
에 async
제너레이터를 사용해 더 간편하게 구현할 수 있습니다.
let range = {
from: 1,
to: 5,
async *[Symbol.asyncIterator]() {
// [Symbol.iterator]: function*()를 짧게 줄임
for (let value = this.from; value <= this.to; value++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield value;
}
},
};
(async () => {
for await (let value of range) {
console.log(value);
}
})();
async 제너레이터 use case
페이지네이션을 이용해 Github 커밋 이력을 페이지별로 확인하는 코드 예시입니다.
- Github에선 커밋 30개의 정보가 담긴 JSON 데이터를 전달하고 다음 페이지에 대한 정보를 Link 헤더에 담아 응답
- 더 많은 커밋 정보 필요시, 헤더에 담긴 링크를 사용해 다음 요청을 전송
const { default: fetch } = require("node-fetch");
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, {
// (1)
headers: { "User-Agent": "Our script" }, // GitHub는 모든 요청에 user-agent헤더를 강제 합니다.
});
const body = await response.json(); // (2) 응답은 JSON 형태로 옵니다(커밋이 담긴 배열).
// (3) 헤더에 담긴 다음 페이지를 나타내는 URL을 추출합니다.
let nextPage = response.headers.get("Link").match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for (let commit of body) {
// (4) 페이지가 끝날 때까지 커밋을 하나씩 반환(yield)합니다.
// 전체 다 반환되면 다음 while (url) 반복문이 트리거되어 서버에 다시 요청을 보냅니다.
yield commit;
}
}
}
(async () => {
let count = 0;
for await (const commit of fetchCommits(
"javascript-tutorial/en.javascript.info"
)) {
console.log(commit.author.login);
if (++count == 100) {
// 100번째 커밋에서 멈춥니다.
break;
}
}
})();
처음에 구상했던 API가 구현되었으며 페이지네이션 관련 내부 메커니즘은 바깥에서 볼 수 없고, 단순히 async 제너레이터만 사용해 원하는 커밋을 반환받을 수 있습니다.
또한 용량이 큰 파일을 다운로드하거나 업로드와 같이 띄엄띄엄 들어오는 데이터 스트림을 다뤄야 할 경우 혹은 사용자 입력 이벤트를 스트림으로 표현하는데, 비동기 제너레이터 혹은 비동기 이터레이터를 사용할 수 있습니다. 참고로 브라우저 등의 몇몇 호스트 환경은 데이터 스트림을 처리할 수 있는 API인 Streams를 제공하기도 합니다.
참고 자료
https://github.com/tc39/proposal-async-iteration