티스토리 뷰

반응형

 

 

지난 두 포스팅에 걸쳐 자바스크립트의 비동기 처리 방법 콜백과 프로미스에 대해 작성했다. 

 

1_ [비동기 |  동기 ] 비동기 처리의 시작 - 콜백 지옥 체험

2_ [자바스크립트 비동기 처리 ] 프로미스(Promise) 개념과 활용

 

이번에는 좀 더 간편하고 깔끔한 코딩이 가능해지는 async, await에 대해 다뤄보겠다. 

 

 


 

프로미스의 체이닝과 같은 콜백 패턴 또한 가독성이 그리 좋지 않다. async, await 은 프로미스의 체이닝 없이 프로미스를 좀 더 간결, 간편하게 동기적으로 실행되는 것처럼 보이게 만들어주는 API이다. 새로운 것이 추가된 것은 아니고 기존 존재하는 프로미스 위에 조금 더 간편한 API를 제공한 것으로 이를 문법적 설탕(syntactic sugar)이라고 한다. 

 


 

 

async 

네트워크 통신을 통해 10초 후에 데이터를 받아온다고 가정해보자. 이를 프라미스를 사용하여 비동기 처리로 받아오는 방법은 다음과 같다. 

// promise
// 네트워크 통신을 통해 10초 후에 데이터를 받아온다고 가정해보자.
async function fetchUserPromise() {
  // do network reqeust in 10 secs....
  return new Promise((resolve, reject) => {
    resolve("Kelly");
  });
}

const user1 = fetchUserPromise();
user1.then(console.log);
console.log(user1); // output : Kelly

 

 

 

이를 async를 통하면 함수 앞에 async를 붙이기만 하면 된다. asycn는 함수 안의 코드 블록들의 반환 값을 resolve 하는 프로미스를 반환한다. 

async function fetchUser() {
  // do network reqeust in 10 secs....
  return "useing async : Kelly";
}

const user = fetchUser();
user.then(console.log);
console.log(user); // Output : useing async : Kelly

 

 

await

await 키워드는 async를 통해 반환된 프로미스가 settled 상태 (비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve 한 처리 결과를 반환한다. await키워드는 반드시 프로미스 앞에서 사용해야 하며 async가 붙은 함수 안에서만 쓸 수 있다. 

 

// 2. await ✨
// async가 붙은 함수 안에서만 쓸 수 있다.

// 정해진 ms가 지나면 resolve 호출 ( 2초 후 🍎)
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getApple() {
  // delay가 끝날 때까지 기다려준다.
  await delay(2000);
  return "🍎";
}

async function getBanana() {
  await delay(1000);
  return "🍌";
}

 

 

만약 프로미스 체이닝을 통해 비동기 처리를 했다면?  

프로미스 체이닝을 통한 코드보다 async와 await을 사용한 코드가 동기적인 것처럼 보이면서 더 직관적으로 이해할 수 있다. 

function getBanana() {
  return delay(3000).then(() => "🍌");
}

 

 

프로미스와 async의 차이 & 병렬적 실행의 예

 

딴 과일들을 받아오는 함수를 만들어보고 이를 프로미스와 async를 이용해서 구현해보겠다.

 

프로미스 체이닝

// 콜백지옥과 비슷한 콜백패턴 문제 발생
function pickFruits() {
  return getApple()
  .then(apple => {
    return getBanana().then(banana => `${apple} + ${banana}`)
  })
}
pickFruits().then(console.log)

// output : 🍎 + 🍌

 

 

async 사용

콜백 패턴이 없어져 더 간결해진다. 하지만 아래 코드도 어찌 됐든 동기적으로 apple과 banna가 실행되기 때문에 총 3초가 걸린다. 

//async를 사용하면 간결해진다. 하지만 이 코드도 apple이 나나와 사과를 받는 일은 서로 연관이 없으므로
// 병렬적으로 실행해줄 필요가 있다.
async function pickFruits() {
  const apple = await applePromise; // 2초
  const banana = await bananaPromise; //1초  총 3초걸림
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);
// output : 🍎 + 🍌

 

 

프로미스 생성으로 병렬적으로 기능 수행

이러한 동기적인 진행방식은 비효율적이므로 이는 프로미스를 만들면 즉시 실행되는 프로미스의 특징을 이용해 프로미스를 생성해준다. 

 

// 병렬적으로 기능 수행
async function pickFruits() {
  // promise를 만드는 순간 바로 실행 된다.
  const applePromise = getApple();
  const bananaPromise = getBanana();
  // 병렬적으로 함수가 실행된다. 총 2초걸림
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);

 

 

 

프로미스 APIs : 병렬적으로 기능을 수행할 때 

 

이처럼 병렬적인 기능을 수행할 때는 각각 프로미스를 만들어주는 것보다 더 간편한 APIs들을 이용하면 수월하게 작업할 수 있다. 프로미스는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가질 수 있다. 프로미스는 5가지의 정적 메서드를 제공하는데 이번에는 유용하게 쓰이는 두 가지 메서드만 다뤄보겠다. 추가적인 메서드는 mdn에서 확인하자.

 

 

1_ Promise.all

여러  개의 비동기 처리를 모두 병렬 처리할 때 사용한다. 바로 위에서 프로미스를 각각 만들어서 처리해 준 것과 같은 기능을 똑같이 수행하지만 코드가 더 간결해진 모습을 볼 수 있다. 

 

promise.all메서드는 모든 프로미스가 모두 fulfilled 상태가 되면 종료한다. 따라서 promise.all가 종료하는 데 걸리는 시간은 가장 늦게 fulfilled 상태가 되는 프로미스의 처리 시간보다 조금 더 길다.  모든 프로미스가 완료되면 resolve 된 처리 결과를 모두 배열에 저장해 새로운 프로미스를 반환한다. 이때 처리 순서가 보장된다. 

 

function pickAllFruits() {
  // Promise.all 이라는 api를 사용하여 모든 프로미스들이 병렬적으로 다 받을 때까지 모아준다.
  // .then 다 받아진 배열이 전달되고 join을 통해 출력
  return Promise.all([getApple(), getBanana()]).then((fruits) =>
    fruits.join(" + ")
  );
}
pickAllFruits().then(console.log);

 

예제 

이처럼 병렬적으로 수행하는 작업에 대해서 모든 프로미스에 await 키워드를 사용할 필요가 없다. 

async function foo() {
  const a = await new Promise((resolve) => setTimeout(() => resolve(1), 3000));
  const b = await new Promise((resolve) => setTimeout(() => resolve(2), 2000));
  const c = await new Promise((resolve) => setTimeout(() => resolve(3), 1000));

  console.log([a, b, c]); // [1,2,3]
}
foo(); // 약 6초 소요

 

Promise.all을 통해 서로 연관이 없는 3개의 비동기 처리는 병렬적으로 처리해준다. 

async function foo() {
  const res = await Promise.all([
    new Promise((resolve) => setTimeout(() => resolve(1), 3000)),
    new Promise((resolve) => setTimeout(() => resolve(2), 2000)),
    new Promise((resolve) => setTimeout(() => resolve(3), 1000)),
  ]);
  console.log(res); // [1,2,3]
}
foo(); // 약 3초소요

 

 

순서가 보장되어야 하는 경우 쓰면 안 된다. 

bar함수는 앞선 비동기 처리의 결과를 가지고 다음 비동기 처리를 수행해야 한다. 따라서 비동기 처리 순서가 보장되어야 하므로 모든 프로미스에 await 키워드를 써 순차적으로 처리할 수밖에 없다. 

// 처리 순서가 보장되어야 할  때 
async function bar(n) {
  const a = await new Promise((resolve) => setTimeout(() => resolve(n), 3000));
  const b = await new Promise((resolve) => setTimeout(() => resolve(a+1), 2000));
  const c = await new Promise((resolve) => setTimeout(() => resolve(b+1), 1000));

  console.log([a, b, c]);
}
bar(1); // 약 6초 소요

 

 

rejected 상태가 있을 때는? 

만약 배열의 프로미스가 하나로도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료한다.

 

 

 

2_ Promise.race 

Promise.all 메서드와 동일하게 프로미스를 요소로 갖는 배열 등의 이 트러블을 인수로 전달받는다. 하지만 promise.all처럼 모든 프로미스가 fulfilled 상태가 되는 것을 기다리는 것이 아니라 가정 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve 하는 새로운 프로미스를 반환한다. 

 

// 어떤 것이든 상관없고 제일 첫 번째 과일을 받아오고 싶다면 ?
// Promise.race api는 배열에 전달 된 프로미스 중에서 가장 먼저 값을 리턴하는 과일만 전달된다. > 바나나 출력
function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log);

 

rejected 상태가 있을때는 ? 

promise.all 메서드와 동일하게 처리된다. 프로미스가 하나라도 rejected 상태가 되면 에러를 reject 하는 새로운 프로미스로 즉시 반환한다.

 

 

 

에러 처리

 

async/await 에서 에러 처리는 try... catch문을 사용할 수 있다. 콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다. 

const Fetch = require("node-fetch");

const foo = async () => {
  try {
    const wrongUrl = "https://wrong.url";

    const response = await fetch(wrongUrl);
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.log(err); // TypeError : Failed to fetch
  }
};
foo();

위 예제 foo 함수의 catch문은 HTTP통신에서 발생한 네트워크 에러뿐 아니라 try코드 블록 내의 모든 문에서 발생한 일반적인 에러까지 모두 캐치할 수 있다. 

 

 

후속처리를 통한 에러 처리

async 함수 내에서 catch문을 사용해서 에러 처리를 하지 않으면 async함수는 발생한 에러를 reject 하는 프로미스를 반환한다. 따라서 async 함수를 호출하고 Propmise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수도 있다. 

 

const Fetch = require("node-fetch");

const foo = async () => {
  const wrongUrl = "https://wrong.url";

  const response = await fetch(wrongUrl);
  const data = await response.json();
  return data;
};

foo().then(console.log).catch(console.log.error); // TypeError: Failed to fetch

 

 

 

 


마무리

세 포스팅에 걸쳐 자바스크립트의 비동기 처리방식에 대해 배워보았다. 이번에 리액트로 사전을 만들면서 리덕스 미들웨어에서 함수를 만들 때  async / await을 썼는데 쓰면서도 그냥 비동기 처리 방식이라고만 이해했고 promise에 대한 개념도 몰랐기 때문에 처리 방식이 어떻게 되는 건지 이해하기가 어려웠다. 하지만 콜백 지옥을 직접 체험하고, 그 콜백 지옥을 promise를 이용해 처리하고, 또 다음에는 async/await으로 처리하는 과정을 거치면서 전반적인 비동기 처리방식을 알게 되었고, 개념만 이해했다고 익숙해지는 것은 아니니 앞으로 더 써보면서 적절한 상황에 promise나 aync / await을 사용하여 여러 방면으로 활용할 줄 알게 해 나가야겠다.

 

 

 

 

 

Reaference:

 

반응형