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

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

Jake Archibald
Jake Archibald

비동기 함수는 Chrome, Edge, Firefox, Safari에서 기본적으로 사용 설정되어 있으며 정말 놀라운 기능입니다. 이 함수를 사용하면 기본 스레드를 차단하지 않고 동기식인 것처럼 프로미스 기반 코드를 작성할 수 있습니다. 비동기 코드를 덜 '똑똑'하고 더 읽기 쉽게 만듭니다.

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

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!';
}

위 작업은 완료하는 데 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());
  }
}
더 깔끔해 보이지만 첫 번째 가져오기를 완전히 읽은 후에야 두 번째 가져오기를 시작할 수 있으며, 이런 식으로 계속됩니다. 이는 병렬로 가져오기를 실행하는 프라미스 예시보다 훨씬 느립니다. 다행히 이상적인 중간 경로가 있습니다.
권장 - 훌륭하게 병렬 처리
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의 폴리필을 가져와 직접 사용하면 됩니다. 다음을 대신해서 사용합니다.

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

폴리필을 포함하고 다음과 같이 작성합니다.

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

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

해결 방법: 리제너레이터

이전 브라우저를 타겟팅하는 경우 Babel은 생성기를 트랜스파일할 수도 있으므로 IE8까지 비동기 함수를 사용할 수 있습니다. 이렇게 하려면 Babel의 es2017 프리셋 es2015 프리셋이 필요합니다.

출력이 그렇게 예쁘지 않으므로 코드 팽창에 주의합니다.

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

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

2014년에 비동기 함수에 대해 정말 큰 기대를 했습니다. 그리고 실제로 브라우저에서 비동기 함수를 사용하는 모습을 보니 기뻐요. 와!