비동기 코드에는 콜백 대신 async 함수 사용하기

비동기 동작을 다룰 때에, 콜백보다는 프로미스를 사용해야 합니다. 이유는 다음과 같습니다.

  • 콜백보다는 프로미스가 코드를 작성하기 쉽습니다.
  • 콜백보다는 프로미스가 타입을 추론하기 쉽습니다.
function fetchPagesCB() {
  let numDone = 0;
  const responses: string[] = [];
  const done = () => {
    const [response1, response2, response3] = responses;
    // ...
  };
  const urls = [url1, url2, url3];
  urls.forEach((url, i) => {
    fetchURL(url, r => {
      responses[i] = url;
      numDone++;
      if (numDone === urls.length) done();
    });
  });
}

위와 같은 콜백 기반의 비동기 함수는 프로미스를 통해 아래와 같은 형태가 될 수 있습니다.

async function fetchPages() {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
  ]);
  // ...
}

그리고 프로미스보다는 async/await를 사용하는 편이 좋습니다. 이유는 아래와 같습니다.

  • 일반적으로 더 간결하고 직관적인 코드가 됩니다.
  • async 함수는 항상 프로미스를 반환하도록 강제합니다.

"함수가 항상 프로미스를 반환하도록 강제"하는 것은 각 함수가 항상 동기 또는 비동기로 실행되어야 한다는 원칙을 쉽게 지키도록 해줍니다. 콜백이나 프로미스를 사용하면 실수로 반(half)동기 코드를 작성할 수 있지만, async 함수에 기반하는 경우 항상 비동기 코드로 작성되기 때문입니다.

아래의 콜백 기반의 함수 fetchWithCache는 얼핏 제대로 만들어진 반동기 함수인 듯 하지만, 실제로 사용할 때 문제를 일으킬 가능성이 있습니다. 캐시가 되어있는 경우에는 callback(..)가 동기적으로 동작할 것이기 때문입니다.

const _cache: {[url: string]: string} = {};

function fetchWithCache(url: string, callback: (text: string) => void) {
  if (url in _cache) {
    callback(_cache[url]);
  } else {
    fetchURL(url, text => {
      _cache[url] = text;
      callback(text);
    });
  }
}

let requestStatus: 'loading' | 'success' | 'error';

// 캐시가 있는 경우 => requestStatus는 'loading'
// 캐시가 없는 경우 => requestStatus는 'success'
function getUser(userId: string) {
  fetchWithCache(`/user/{userId}`, profile => {
    requestStatus = 'success';
  });
  requestStatus = 'loading';
}

이를 async 함수로 대체하면 보다 간결하고, 일관적인 형태로 사용할 수 있게 됩니다.

const _cache: {[url: string]: string} = {};

async function fetchWithCache(url: string) {
  if (url in _cache) {
    return _cache[url];
  }
  const response = await fetch(url);
  const text = await response.text();
  _cache[url] = text;
  return text;
}

let requestStatus: 'loading' | 'success' | 'error';

async function getUser(userId: string) {
  requestStatus = 'loading';
  const profile = await fetchWithCache(`/user/{userId}`);
  requestStatus = 'success';
}

한가지 유의점으로, async 함수 내에서 프로미스를 반환한다고 해서 Promise<Promise<T>> 반환타입이 되지는 않습니다. 이 경우에도 동일하게 Promise<T>가 됩니다. 타입 체커를 통해서도 이를 확인할 수 있습니다.

// Function getJSON(url: string): Promise<any>
async function getJSON(url: string) {
  const response = await fetch(url);
  const jsonPromise = response.json();  // Type is Promise<any>
  return jsonPromise;
}