for await...of

for await...of 문은 동기 이터러블뿐만 아니라 비동기 이터러블 객체를 순환하는 루프를 생성합니다. 이 문은 비동기 함수 본문 내부 와 모듈 내부를 포함하여 await를 사용할 수 있는 맥락에서만 사용할 수 있습니다.

시도해보기

구문

js
for await (variable of iterable)
  statement
variable

반복할 때마다 시퀀스에서 값을 받습니다. const, let, 또는 var를 사용한 선언이거나, 할당 연산자의 대상일 수 있습니다. (예: 이전에 선언된 변수 또는 객체 속성) var로 선언된 변수는 루프에 대한 로컬이 아닙니다. 즉, 해당 변수는 for await...of 루프가 있는 동일한 범위에 있습니다.

iterable

비동기 이터러블 또는 동기 이터러블입니다. 루프가 작동하는 값 시퀀스의 소스입니다.

statement

반복할 때마다 실행할 명령문입니다. variable을 참조할 수 있습니다. 블록 문을 사용하여 여러 문을 실행할 수 있습니다.

설명

for await...of 루프가 이터러블을 반복할 때, 먼저 비동기 이터레이터를 반환하는 이터러블의 [@@asyncIterator]() (en-US) 메서드를 가져와 호출합니다. @asyncIterator 메서드가 없으면 동기 이터레이터를 반환하는 [@@iterator]() 메서드를 찾습니다. 그런 다음 반환된 동기 이터레이터는 next(), return(), 그리고 throw() 메서드에서 반환된 모든 객체를 이행되거나 거부된 프로미스로 래핑하여 비동기 이터레이터로 래핑합니다. value 속성도 프로미스인 경우 이행됩니다. 그런 다음 루프는 최종 비동기 이터레이터의 next() 메서드를 반복적으로 호출하고 반환된 프로미스를 await하며 변수에 할당할 일련의 value를 생성합니다.

for await...of 루프가 일찍 종료되면(예: break 문이 발생하거나 오류가 발생한 경우) 정리를 수행하기 위해 이터레이터의 return() 메서드가 호출됩니다. 반환된 프로미스는 루프가 종료되기 전에 await됩니다.

for await...of는 일반적으로 for...of 루프와 동일하게 기능하며 동일한 구문과 의미를 공유합니다. 아래의 몇 가지 차이점은 있습니다.

  • for await...of는 동기와 비동기 이터러블 모두에서 작동하지만 for...of는 동기 이터러블에서만 작동합니다.
  • for await...of비동기 함수 본문 내부와 모듈을 포함하여 await를 사용할 수 있는 컨텍스트에서만 사용할 수 있습니다. 이터러블이 동기인 경우에도 루프는 여전히 매 순회마다 반환 값을 await하므로 반복되는 프로미스의 언래핑으로 인해 실행 속도가 느려집니다.
  • iterable이 프로미스를을 yield하는 동기 이터러블인 경우 for await...of는 일련의 이행된 값을 생성하는 반면 for...of는 일련의 프로미스를 생성합니다. (그러나 오류 처리와 정리에 주의하세요. 동기 이터러블과 제너레이터에 순회 참조.)
  • for await...of의 경우 variableasync가 식별자 일 수 있습니다(예: for await(async of foo)). for...of는 이를 금지합니다.

예제

비동기 이터러블 순회

비동기 이터러블 프로토콜을 명시적으로 구현하는 객체를 반복할 수도 있습니다.

js
const LIMIT = 3;

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      next() {
        const done = i === LIMIT;
        const value = done ? undefined : i++;
        return Promise.resolve({ value, done });
      },
      return() {
        // 여긴 소비자가 루프 초기에 'break' 또는 'return'을 호출한 경우에 도달합니다.
        return { done: true };
      },
    };
  },
};

(async () => {
  for await (const num of asyncIterable) {
    console.log(num);
  }
})();
// 0
// 1
// 2

비동기 제너레이터 순회

비동기 제너레이터 함수의 반환 값은 비동기 이터러블 프로토콜을 준수하므로 for await...of를 사용하여 반복할 수 있습니다.

js
async function* asyncGenerator() {
  let i = 0;
  while (i < 3) {
    yield i++;
  }
}

(async () => {
  for await (const num of asyncGenerator()) {
    console.log(num);
  }
})();
// 0
// 1
// 2

for await...of를 사용하여 비동기 제너레이터를 반복하는 보다 구체적인 예제를 위해 API에서 데이터를 순회하는 것을 생각해 보겠습니다.

이 예제는 먼저 데이터 스트림에 대한 비동기 이터러블을 생성한 다음, 이를 사용하여 API 응답의 크기를 찾습니다.

js
async function* streamAsyncIterable(stream) {
  const reader = stream.getReader();
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) return;
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

// URL에서 데이터를 가져오고 비동기 제너레이터를 사용하여 응답 크기를 계산합니다.
async function getResponseSize(url) {
  const response = await fetch(url);
  // 응답 크기를 바이트 단위로 유지.
  let responseSize = 0;
  // for-await-of 루프. 비동기는 응답의 각 부분을 순회.
  for await (const chunk of streamAsyncIterable(response.body)) {
    // 총 응답 길이 증가.
    responseSize += chunk.length;
  }

  console.log(`Response Size: ${responseSize} bytes`); // "Response Size: 1071472"
  return responseSize;
}
getResponseSize("https://jsonplaceholder.typicode.com/photos");

동기 이터러블과 제너레이터 순회

for await...of 루프에서 동기 이터러블과 동기 제너레이터도 사용할 수 있습니다. 이 경우, 산출된 값을 루프 제어 변수에 할당하기 전에 내부적으로 await합니다.

js
function* generator() {
  yield 0;
  yield 1;
  yield Promise.resolve(2);
  yield Promise.resolve(3);
  yield 4;
}

(async () => {
  for await (const num of generator()) {
    console.log(num);
  }
})();
// 0
// 1
// 2
// 3
// 4

//  for-of 루프와 비교

for (const numOrPromise of generator()) {
  console.log(numOrPromise);
}
// 0
// 1
// Promise { 2 }
// Promise { 3 }
// 4

참고: 동기 제너레이터에서 거부된 프로미스 yield에 유의하세요. 이 경우 for await...of는 거부된 프로미스를 처리할 때 throw를 하고, 해당 제너레이터의 finally 블록을 호출하지 않습니다. 이것은 할당된 일부 리소스를 try/finally로 해제해야 하는 경우에 바람직하지 않을 수 있습니다.

js
function* generatorWithRejectedPromises() {
  try {
    yield 0;
    yield 1;
    yield Promise.resolve(2);
    yield Promise.reject(3);
    yield 4;
    throw 5;
  } finally {
    console.log("called finally");
  }
}

(async () => {
  try {
    for await (const num of generatorWithRejectedPromises()) {
      console.log(num);
    }
  } catch (e) {
    console.log("caught", e);
  }
})();
// 0
// 1
// 2
// caught 3

// for-of 루프와 비교

try {
  for (const numOrPromise of generatorWithRejectedPromises()) {
    console.log(numOrPromise);
  }
} catch (e) {
  console.log("caught", e);
}
// 0
// 1
// Promise { 2 }
// Promise { <rejected> 3 }
// 4
// caught 5
// called finally

동기 제어레이터의 finally 블록을 항상 호출되게 만드려면, 루프의 적절한 형식(비동기 제너레이터의 경우 for await...of, 동기 제너레이터의 경우 for...of)을 사용하고 루프 내에서 명시적으로 yield된 프로미스를 await해야 합니다.

js
(async () => {
  try {
    for (const numOrPromise of generatorWithRejectedPromises()) {
      console.log(await numOrPromise);
    }
  } catch (e) {
    console.log("caught", e);
  }
})();
// 0
// 1
// 2
// caught 3
// called finally

명세서

Specification
ECMAScript Language Specification
# sec-for-in-and-for-of-statements

브라우저 호환성

BCD tables only load in the browser

같이 보기