Promise와 async, await

불꽃남자·2021년 1월 23일
1

TFT 전적 검색 웹을 만들면서 API를 요청할 때에 프로미스 체이닝의 사용이 거의 강제되었다. 근데 생각대로 잘 안 되었다.
분명 이전에 Promise와 async, await 같은 비동기 지원 함수에 대해 포스팅을 했었다. 그런데도 해멘 것은 완벽히 이해하지 못 했다는 뜻이다. 또 이 프로젝트는 TypeScript도 같이 적용했기 때문에 더 헷갈렸다.

이제는 내가 생각한 대로 운용이 가능해져서 글로 남긴다.

1. Promise가 있기 전에

일단 비동기에 대해 간단히 설명하겠다.


여기 JS의 대표적인 비동기 함수인 setTimeout이 있다. 0.5초뒤에 Cherry를 출력하고 뒤이어 Melon을 출력하고자 한다.

setTimeout(() => {
  console.log("Cherry");
}, 500);

console.log("Melon");

하지만 결과는 Melon이 출력되고 0.5초 후 Cherry가 출력되었다. 이처럼 특정 코드의 실행이 끝날 때 까지 기다려주지 않고 다음 코드를 실행하는 것이 바로 비동기 처리이다.

이제 비동기적으로 응답하는 API를 요청하는 예제를 살펴보자.
요새는 잘 안 쓰이지만, jquery의 ajax를 예로 들어보자.

const getUsers = () => {
  const Users = [];
  $.ajax({
    type: "GET",
    url: `http://someAPI/users`,
    dataType: "json",
    data: {},
  })
  .done((response) => {
    Users = response;
  });

  return Users;
}

console.log(getUsers); // undefined

API로부터 받아온 response가 출력되리라 기대했지만, 출력되는 것은 undefined이다. 왜냐? $.get 함수가 결과를 응답하기까지 기다리지 않고 return Users 코드가 바로 실행되었기 때문이다.

이를 콜백 함수로 해결해왔으나, 콜백 함수는 중첩되면 될수록 가독성이 심하게 떨어진다는 단점이 있다. 그리고 Promise가 등장한다.

2. Promise

Promise는 동기적으로 작동하는데 비동기적으로 보인다.

위의 ajax 예제에 Promise를 적용해보겠다.

const getUsers = () => {
  return new Promise((resolve, reject) => {
    $.ajax({
      type: "GET",
      url: `http://someAPI/users`,
      dataType: "json",
      data: {},
    })
    .done((response) => {
      resolve(response);
    })
    .fail((xhr, status, error) => {
      reject(error);
    })
  });
}

getUsers()
  .then((response) => { console.log(response); })
  .catch((error) => { console.log(error); });

이러면 정상적으로 Users가 출력된다. 왜 그럴까?

getUsers 함수는 Promise를 반환한다. Promise는 매개변수로 executor 라는 이름의 콜백함수를 갖는데, Promise의 구현과 동시에 executor 함수가 실행된다.
executor는 resolve 함수와 reject 함수를 매개변수로 갖는다. resolve 함수가 실행되면 Promise가 잘 이행되었다는 것이고, reject 함수가 실행되면 Promise가 이행되지 못 했다는 것이다.

그리고 반환된 Promise 뒤에는 then 함수와 catch 함수가 올 수 있다.

then과 catch는 Promise가 이행될 때 까지(resolve나 reject가 실행될 때 까지) 기다린다.

then 함수는 Promise가 fulfill(즉, Promise의 resolve 함수가 실행되었을 때)되면 실행된다. then 함수는 매개변수로 onResolve를 받는데, 이는 해당 Promise가 resolve 함수를 실행할 때의 인수이다.
위의 예제에서는 API의 응답 결과인 response가 resolve 되었으니 응답 결과가 출력된다.

catch 함수는 Promise가 reject되면 실행된다. catch 함수도 then 함수와 마찬가지로 해당 Promise의 onReject를 매개변수로 받는다.
위 예제에서는 API 요청이 실패했을 때 받는 error를 매개변수로 받아와 출력한다.

자, 이제 예제의 코드를 읽어보자.

getUsers 함수가 실행되면 Promise가 반환되면서 executor가 실행된다. someAPI에 ajax로 get요청을 보내고, 요청에 성공하면 응답 결과를 resolve하고, 요청에 실패하면 오류 내용을 reject한다.

resolve가 실행되면 then이 실행되어 API 응답 결과가 출력되고, reject가 실행되면 catch가 실행되어 오류 내용이 출력된다.

자세한 것은 MDN의 Promise DocsPromise 생성자 Docs에 나와있다.

이게 Promise의 기본적인 개념이다.

3. async await

그리고 그것보다 더 읽기 쉽게끔 만들어 주는 문법이 async와 await다.
async와 await는 새로운 것은 아니고 Promise와 then, catch를 기반으로 만들어진 문법이다.

위에서 소개한 Promise 예제에 async await를 적용해보겠다.

const getUsers = () => {
  return new Promise((resolve, reject) => {
    $.ajax({
      type: "GET",
      url: `http://someAPI/users`,
      dataType: "json",
      data: {},
    })
    .done((response) => {
      resolve(response);
    })
    .fail((xhr, status, error) => {
      reject(error);
    })
  });
}

const prtinUsers = async () => {
  try {
    const users = await getUsers();
    console.log(users);
  } catch(error) {
    console.log(error);
  }
}

async는 함수의 선언 앞에 붙는다. async를 붙여 선언한 함수는 AsyncFunction이 된다.
또한 asyncFunction이 return하는 값은 Promise.resolve()으로 onResolve한 것과 동일하다.

await는 AsyncFunction내부에서만 사용 가능한 문법이다. await 뒤에는 Promise가 와야 한다. await 뒤에 Promise가 오면, Promise가 이행될 때까지 기다린다.
위 예제같은 경우 getUsers 함수가 반환하는 Promise가 실행될 때 까지 기다리고, resolve된 값을 users 변수에 담아 출력한다.

async/await 문법은 then/catch 문법에 비해 최근에 나왔다. 뭐가 더 좋다고 얘기하기는 어렵지만, 나에게는 async/await 문법이 코드를 읽기 더 쉽다고 느껴져서 선호된다.

4. 내 프로젝트에 Promise와 then/catch를 적용

그리고 이걸 내 프로젝트에 적용했다. 그리고 그 결과가 이거다.

import axios, { AxiosResponse } from "axios";

import { API_KEY } from "../config";
import { RankEntryResponseT, SummonerPayloadT, SummonerInfoResponseT, SummonerResponseT } from "../types/types";

export const getSummoner = ( summonerPayload: SummonerPayloadT ): Promise<SummonerResponseT> => {
  const { summonerName } = summonerPayload;

  const TFT_API = axios.create({
    baseURL: `/tft`,
    headers: {
      "X-Riot-Token": API_KEY
    }
  })

  return new Promise((resolve, reject) => {
      TFT_API
        .get(`/summoner/v1/summoners/by-name/${summonerName}`)
        .then((summonerInfoResponse: AxiosResponse<Promise<SummonerInfoResponseT>>): Promise<SummonerInfoResponseT> => {
          return new Promise((resolve) => {
            resolve(summonerInfoResponse.data);
          })
        })
        .then((summonerInfoResponseData: SummonerInfoResponseT) => {
          const encryptedSummonerId = summonerInfoResponseData.id;

          TFT_API
            .get(`/league/v1/entries/by-summoner/${encryptedSummonerId}`)
            .then((rankEntryResponse: AxiosResponse<RankEntryResponseT[]>) => {
              resolve({
                summonerInfo: summonerInfoResponseData,
                rankEntry: rankEntryResponse.data
              })
            })
        })
        .catch((err) => reject(err));
  });
}

대충 봐도 읽기 좋은 코드는 아니다. 작동은 할지언정 나쁜 코드다. axios가 Promise를 반환하는 것이 더 혼동을 주기도 했고, 얘네가 뭘 반환하는지 제대로 모르다 보니 TypeScript를 사용하는 부분에서도 너무 헷갈렸다. TypeScript를 처음 쓰는 거라서 그것도 어려웠다.

아무튼... 이걸 async/await로 고쳐야겠다 생각했다.

4.1. async/await로 리팩토링

핵심은 axios 요청이 Promise를 반환한다는 것, async 함수가 반환하는 값은 Promise.resolve라는 것 이다.

그리고 아래가 async/await로 고친 결과다.

import axios, { AxiosResponse } from "axios";

import { API_KEY } from "../config";
import { RankEntryResponseT, SummonerPayloadT, SummonerInfoResponseT, SummonerResponseT } from "../types/types";

export const asyncGetSummoner = async (summonerPayload: SummonerPayloadT): Promise<SummonerResponseT> => {
  const { summonerName } = summonerPayload;

  const TFT_API = axios.create({
    baseURL: `/tft`,
    headers: {
      "X-Riot-Token": API_KEY
    }
  })

  const summonerInfoResponse: AxiosResponse<SummonerInfoResponseT> = await TFT_API.get(`/summoner/v1/summoners/by-name/${summonerName}`);
  const encryptedSummonerId = summonerInfoResponse.data.id;

  const rankEntryResponse: AxiosResponse<RankEntryResponseT[]> = await TFT_API.get(`/league/v1/entries/by-summoner/${encryptedSummonerId}`);

  const summonerResponse: SummonerResponseT = {
    summonerInfo: summonerInfoResponse.data,
    rankEntry: rankEntryResponse.data
  };

  return summonerResponse;
}

어떤가? 훨씬 보기 낫지 않은가? 나는 훨씬 코드를 읽기 좋아져서 굉장히 흡족했다.

아무튼... 이게 다다.

🍓

이 글을 포스팅하게 된 계기는 내가 Promise에 대해 한 단계 더 깊게 이해하게 된 것을 기록하고 싶어서이다. 읽기 힘들었던 API요청 코드를 성공적으로 리팩토링 할 수 있어서 좋았다.
물론 지식이 늘어나면 더 깎아낼 부분이 보일 것이다.

profile
프론트엔드 꿈나무, 탐구자.

0개의 댓글