비동기 함수: 프로미스 친화적 만들기

비동기 함수를 사용하면 마치 동기 함수인 것처럼 프로미스 기반 코드를 작성할 수 있습니다.

Jake Archibald
Jake Archibald

비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되어 있습니다. 솔직히 상당히 놀라운 것입니다. 이를 통해 프라미스 기반 코드를 동기적이었지만 기본 스레드를 차단하지 않은 경우. 그들은 덜 '똑똑한' 비동기 코드 더 쉽게 읽을 수 있습니다.

비동기 함수는 다음과 같이 작동합니다.

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

함수 정의 앞에 async 키워드를 사용하면 함수 내의 await. 프로미스를 await하면 함수가 일시중지됩니다. 프라미스가 해결될 때까지 비블로킹 방식으로 작동합니다. 프라미스가 이행되면 반환합니다. 프라미스가 거부되면 거부된 값이 발생합니다.

브라우저 지원

브라우저 지원

  • Chrome: 55. <ph type="x-smartling-placeholder">
  • Edge: 15. <ph type="x-smartling-placeholder">
  • Firefox: 52. <ph type="x-smartling-placeholder">
  • Safari: 10.1. <ph type="x-smartling-placeholder">

소스

예: 가져오기 로깅

URL을 가져오고 응답을 텍스트로 로깅한다고 가정해 보겠습니다. 다음과 같이 표시됩니다. 사용할 수 있습니다.

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

다음은 비동기 함수를 사용하는 경우에도 동일합니다.

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

줄 수는 동일하지만 콜백이 모두 사라졌습니다. 이렇게 하면 특히 프라미스에 익숙하지 않은 경우에 유용합니다.

비동기 반환 값

비동기 함수는 await 사용 여부와 관계없이 항상 프로미스를 반환합니다. 그 것이 프라미스는 비동기 함수가 반환하는 것과 함께 해결되거나 비동기 함수가 발생한다는 것을 의미합니다. 따라서

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

hello()를 호출하면 "world"처리하는 프로미스가 반환됩니다.

async function foo() {
  await wait(500);
  throw Error('bar');
}

foo()를 호출하면 Error('bar')와 함께 거부되는 프로미스가 반환됩니다.

예: 응답 스트리밍

비동기 함수의 이점은 더 복잡한 예에서 증가합니다. 당신이 원한다고 말해 보세요 응답을 스트리밍하고 최종 크기를 반환합니다.

다음은 프라미스를 사용하는 예입니다.

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

제이크 '프라미스 행사자' 좀 보세요. 아치볼드. 내 전화 사용 방법 보기 processResult() 자체 내에서 비동기 루프를 설정하나요? 글을 쓰면서 아주 똑똑하다고 생각해요. 그러나 대부분의 '똑똑한' 살펴봐야 하기 때문에 무슨 일을 하는지 알아내려고 합니다. 90년대입니다.

비동기 함수를 이용해 다시 시도해 보겠습니다.

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

모든 '스마트한' 사라졌습니다. 비동기 루프를 통해 자신이 너무 자신 있게 느끼게 해줬고 믿음직하고 지루한 while 루프로 바뀌었습니다. 훨씬 낫습니다. 앞으로는 비동기 반복자, 그 것은 while 루프를 for-of 루프로 교체하여 더 깔끔하게 만듭니다.

기타 비동기 함수 문법

이미 async function() {}을(를) 보여드렸지만 async 키워드는 다음과 같을 수 있습니다. 다른 함수 구문과 함께 사용됩니다.

화살표 함수

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});
드림

객체 메서드

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

클래스 메서드

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();
드림

단, 너무 순차적이지 않도록 하세요

동기처럼 보이는 코드를 작성하고 있지만 동시에 작업을 수행할 수 있는 기회를 제공합니다.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

위의 코드는 완료하는 데 1, 000ms가 걸립니다.

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

위 작업은 완료하는 데 500밀리초가 걸립니다. 두 대기가 동시에 발생하기 때문입니다. 실제 예를 살펴보겠습니다.

예: 가져오기(fetch)를 순서대로 출력

일련의 URL을 가져와서 최대한 빨리 올바른 순서로 정렬해야 합니다.

심호흡 - 프라미스를 사용하면 다음과 같이 됩니다.

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

예, 맞습니다. 저는 reduce를 사용하여 프로미스 시퀀스를 연결합니다. 좋아요 스마트를 사용합니다. 하지만 이 방법은 아주 스마트한 코딩이므로 사용하지 않는 편이 유리합니다.

그러나 위의 함수를 비동기 함수로 변환할 때는 지나치게 순차적인 경우:

권장되지 않음 - 너무 순차적임
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
드림 <ph type="x-smartling-placeholder"></ph> 훨씬 깔끔해 보이지만 첫 번째 가져오기가 완료되기 전에는 두 번째 가져오기가 시작되지 않습니다. 완전히 읽히는 등의 식입니다. 이것은 프라미스 예제보다 훨씬 느립니다 가져오기를 병렬로 수행합니다. 다행히 이상적인 중간 경로가 있습니다.
권장 - 훌륭하게 병렬 처리
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
드림 <ph type="x-smartling-placeholder"></ph> 이 예에서는 URL을 동시에 가져와서 읽지만 reduce 비트가 읽기 쉬운 표준적이고 지루한 for 루프로 대체됩니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

브라우저 지원 해결 방법: 생성기

제너레이터를 지원하는 브라우저를 대상으로 하는 경우( 모든 주요 브라우저의 최신 버전을 )를 사용하여 비동기 함수를 폴리필(polyfill)할 수 있습니다.

Babel이 이 작업을 대신 수행합니다. Babel REPL을 통한 예시

를 통해 개인정보처리방침을 정의할 수 있습니다.

트랜스파일 방식을 추천합니다. 대상 브라우저가 비동기 함수를 지원하지만 실제로 트랜스파일러를 사용하면 Babel의 폴리필 직접 사용해 보세요. 다음을 대신해서 사용합니다.

async function slowEcho(val) {
  await wait(1000);
  return val;
}

polyfill을 포함하면 됩니다. 그리고 다음과 같이 작성합니다.

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

생성기 (function*)를 createAsyncFunction에 전달해야 합니다. 그리고 await 대신 yield를 사용합니다. 그 외에는 동일하게 작동합니다.

해결 방법: 리제너레이터

오래된 브라우저를 대상으로 하는 경우 Babel은 생성기를 트랜스파일할 수도 있습니다. IE8까지 비동기 함수를 사용할 수 있게 해줍니다. 이렇게 하려면 Babel의 es2017 프리셋 es2015 프리셋도 있습니다.

출력이 그렇게 예쁘지 않으므로 있습니다.

모든 것을 비동기화합니다.

모든 브라우저에서 비동기 함수가 지원되면 프라미스 반환 함수! 코드를 더 깔끔하게 만들 뿐만 아니라 함수가 항상 프로미스를 반환하도록 합니다.

저는 비동기 함수에 다시 와서 2014년 실제로 브라우저에서 방문하게 되어 매우 기쁩니다. 와!