9장 비동기 함수, 이터레이터, 제너레이터

2023.06.16

9.1 async 함수

  • async/await 구문은 프라미스를 생성하고 소비하기 위한 그냥 문법적 설탕이지만 비동기 코드를 작성하는 방법을 완전히 변환하여 동기 흐름만 작성하고 비동기 부분을 콜백 대신 논리적 흐름으로 작성할 수 있다.
  • async/await는 근본적으로 비동기 코드 작성을 변경하고 단순화한다.
  • async 함수의 네 가지 주요 기능은 다음과 같다.
    • async 함수는 암시적으로 프라미스를 만들고 반환한다.
    • async 함수에서 await는 프라미스를 사용하여 코드가 프라미스가 확정될 때까지 비동기식으로 대기하는 지점을 표시한다.
    • 함수가 프라미스가 확정되기를 기다리는 동안 스레드는 다른 코드를 실행할 수 있다.
    • async 함수에서 전통적으로 동기식으로 여겨졌던 코드는 await를 포함하는 경우 비동기식이다.
    • 예외는 거부이고 거부는 예외다. 반환은 확정이고 완료는 결과다.(즉, 프라미스는 await하는 경우 프라미스의 이행 값을 await 표현식의 결과로 본다).

9.1.1 async 함수는 프라미스를 만든다.

  • async 함수는 함수 내의 코드를 기반으로 해당 프라미스를 확인하거나 거부하면서 프라미스를 생성하고 반환한다.
  • 코드의 동기 부분에서 발생하는 모든 오류는 동기 예외가 아니라 거부로 전환된다.
1async function example() {
2  return 1;
3}
4example(); // Promise {<fulfilled>: 1}
5
6async function rejectExample() {
7  throw new Error('error throwing');
8  return 1;
9}
10rejectExample(); // Promise {<rejected>: Error: error throwing ..

9.1.2 async 함수는 프라미스를 사용한다.

  • awit 연산자는 await가 사용되는 모든 곳에서 피연산자의 값에 대해 Promise.resolve를 효과적으로 사용하여 이를 수행한다.

9.1.3 await가 사용될 때 표준 로직은 비동기적이다.

  • async 함수에서 await가 들어간 경우 전통적으로 동기적 코드는 비동기적이다.
1async function fetchInSeries(urls) {
2  const results = [];
3  for (const url of urls) {
4    const response = await fetch(url);
5    if (!response.ok) {
6      throw new Error('HTTP error ' + response.status);
7    }
8    results.push(await response.json());
9  }
10  return results;
11}
12(async () => {
13  try {
14    const results = await fetchInSeries(['1.json', '2.json', '3.json']);
15    console.log(results);
16  } catch (err) {
17    console.error(err);
18  }
19})();
  • for-of 루프내에서 await가 사용되기 때문에 루프가 비동기적으로 실행된다.

9.1.4 예외는 거부이고 거부는 예외다. 반환은 확정이고 완료는 결과다.

  • 거부는 예외다. 프라미스를 기다리고 프라미스가 거부되면 예외로 바뀌고 try/catch/finally를 사용하여 확인할 수 있다.
  • 예외는 거부다. async 함수에서 throw하면 함수의 프라미스를 거부하는 것으로 변환된다.
  • return은 확정이다. async 함수에서 return하는 경우 return을 제공ㅎ안 피연산자의 값으로 프라미스를 이행된다.

9.1.5 async 함수의 병렬 작업

1const data = [await fetchJSON(a), await fetchJSON(b), await fetchJSON(c)];
2
3// 병렬로 수행하려면 위처럼 하면 안 된다.
  • 자바스크립트 엔진은 배열을 생성하고 데이터 변수에 할당하기 전에 배열의 내용을 구성하는 표현식을 평가하므로 함수는 해당 프라미스가 완료될 때까지 직렬로 수행된다.
  • await Promise.all을 통해 병렬로 실행할 수 있다.

9.1.6 await를 반환할 필요가 없다.

  • async 함수는 반환 값을 사용하여 함수가 생성한 프라미스를 확정하므로 반환값이 thenable이면 이미 사실상 기다린다.
  • return await는 await await와 약간 비슷하다.
  • 피연산자가 네이티브 프라미스가 아닌 thenable인 경우, 하나의 비동기 주기(또는 틱)을 유지하는 추가 프라미스 확인 계층이 추가된다. 즉, 대기가 있는 버전은 대기가 없는 버전보다 약간 늦게 완료된다.
1function thenableResolve(value) {
2  return {
3    then(onFulfilled) {
4      // A thenable may call its callback synchronously like this;
5      // a native promise never will.
6      onFulfilled(value);
7    },
8  };
9}
10async function a() {
11  return await thenableResolve('a');
12}
13async function b() {
14  return thenableResolve('b');
15}
16a().then((value) => console.log(value));
17b().then((value) => console.log(value));
  • ES2020에는 return await nativePromise가 추가 비동기 틱을 제거하도록 최적화하는 사양 변경이 포함되어 있고 이미 자바스크립트 엔진에 적용되고 있다.
    • node > 13
    • 최신버전의 크롬, 크로미움
1async function a() {
2  return await Promise.resolve('a');
3}
4async function b() {
5  return Promise.resolve('b');
6}
7a().then((value) => console.log(value));
8b().then((value) => console.log(value));

9.1.7 함정: 예기치 않은 장소에서 비동기 함수 사용

1array.filter(async (entry) => {
2  const response = await fetch(entry.url);
3  const keep = response.ok ? (await response.json()).keep : false;
4  return keep;
5});
  • 위의 필터 함수에는 여전히 배열의 모든 값이 존재하고 임포트 작업이 완료될 때까지 기다리지 않는다.
    • async 함수는 항상 프라미스를 반환하고 filter는 프라미스를 기대하지 않고 keep 플래그를 기대하기 때문이다.
  • 작성 중인 함수가 콜백일 때 해당 콜백의 반환값이 사용되는 방식을 고려해야 한다.

9.2 비동기 이터레이터, 이터러블, 제너레이터

  • 비동기 이터레이터는 결과 객체를 직접 반환하는 대신 결과 객체의 프라미스를 반환하는 이터레이터이다.
  • 비동기 이터러블에는 비동기 이터레이터를 반환하는 Symbol.asyncIterator가 있다.
  • 비동기 제너레이터 함수는 값 자치게 아닌 값의 프라미스를 생성하는 비동기 제너레이터를 만든다.

9.2.1 비동기 이터레이터

  • 대부분의 경우 비동기 이터레이터를 원할 때 비동기 제너레이터 함수를 작성하는 것이 가장 좋다.
  • 비동기 이터레이터는 이터레이터와 동일한 방식으로 작동한다.
    • 모든 비동기 이터레이터는 %AsyncIteratorPrototype%을 호출하는 객체에서 상속한다.
    • %IteratorPrototype%과 마찬가지로 %AsyncIteratorPrototype%를 공개적으로 접근할 수 있는 전역 또는 속성이 없다.
    • %IteratorPrototype%보다 접근하기가 조금 더 어색하다.
    1const asyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}));
  • 일반적으로 비동기 이터레이터를 수동으로 생성하지 않는 것이 좋다. 비동기 제너레이터 함수를 사용하자.

9.2.3 비동기 제너레이터

  • 비동기 제너레이터 함수는 async 함수와 제너레이터 함수의 조합이다.
1async function* fetchInSeries([...urls]) {
2  for (const url of urls) {
3    const response = await fetch(url);
4    if (!response.ok) {
5      throw new Error('HTTP error ' + response.status);
6    }
7    yield response.json();
8  }
9}
  • 제너레이터는 %AsyncIteratorPrototype%에서 상속되는 %AsyncGeneratorPrototype%에서 상속되는 적절한 프로토타입인 fetchInSeries.prototype을 자동으로 가져온다.
  • 비동기 제너레이터 함수에서 yield는 사용자가 제공한 모든 피연산자에 await를 자동으로 적용한다.

9.2.3 for-await-of

  • 비동기 이터레이터를 더 편리하게 사용하기 위한 for-await-of 루프가 있다.
1for await (const value of fetchInSeries([a, b, c])) {
2  console.log(value);
3}

9.3 과거 습관을 새롭게

9.3.1 명시적 프라미스와 then/catch 대신 비동기 함수를 사용

  • async/await를 사용하여 비동기 부분에 대한 콜백을 사용하지 않고도 코드의 논리를 작성할 수 있다.