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

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

제이크 아치볼드
제이크 아치볼드

비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되며 솔직히 놀랍습니다. 이 함수를 사용하면 기본 스레드를 차단하지 않고도 동기식인 것처럼 promise 기반 코드를 작성할 수 있습니다. 이러한 함수를 사용하면 비동기 코드가 덜 '똑똑'해지고 가독성이 높아집니다.

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

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

함수 정의 앞에 async 키워드를 사용하는 경우 함수 내에서 await를 사용할 수 있습니다. 프로미스를 await하면 함수는 프로미스가 결정될 때까지 비차단 방식으로 일시중지됩니다. 프로미스가 충족되면 값을 반환합니다. 프로미스가 거부되면 거부된 값이 발생합니다.

브라우저 지원

브라우저 지원

  • 55
  • 15
  • 52
  • 10.1

소스

예: 가져오기 로깅

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);
  }
}

줄 수는 동일하지만 모든 콜백이 사라졌습니다. 이렇게 하면 특히 Promise에 익숙하지 않은 경우 훨씬 쉽게 읽을 수 있습니다.

비동기 반환 값

비동기 함수는 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!';
}

위의 작업은 두 대기가 동시에 발생하므로 완료하는 데 500ms가 걸립니다. 실제 예를 살펴보겠습니다.

예: 가져오기를 순서대로 출력하기

일련의 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());
  }
}
훨씬 깔끔해 보이지만 첫 번째 가져오기를 완전히 읽은 후에 두 번째 가져오기가 시작되지 않습니다. 이는 병렬로 가져오기를 실행하는 프로미스 예시보다 훨씬 느립니다. 다행히 이상적인 중간 조건이 있습니다.
권장 - 훌륭하고 병렬 처리
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);
  }
}
이 예에서는 URL을 가져오고 동시에 읽습니다. 하지만 '스마트' reduce 비트는 지루하고 읽기 쉬운 표준 for 루프로 대체됩니다.

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

생성기를 지원하는 브라우저(모든 주요 브라우저의 최신 버전 포함)를 타겟팅하는 경우 비동기 함수를 폴리필(polyfill)할 수 있습니다.

Babel이 이 작업을 자동으로 수행합니다. Babel REPL을 통한 예시는 다음과 같습니다.

대상 브라우저에서 비동기 함수를 지원하면 사용 중지할 수 있으므로 트랜스파일 방식을 사용하는 것이 좋습니다. 그러나 실제로 트랜스파일러를 사용하고 싶지 않다면 Babel의 polyfill을 직접 사용하면 됩니다. 다음을 대신해서 사용합니다.

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 사전 설정이 필요합니다.

출력이 그렇게 깔끔하지 않으므로 코드가 팽창되지 않도록 주의하세요.

모든 것을 비동기화하세요!

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

저는 2014년에 비동기 함수를 사용하게 되어 매우 기뻤는데, 브라우저에서 실제로 이러한 함수가 나오게 되어 기쁩니다. 와!