비동기 처리

먼저 다음과 같이 비동기 처리를 하지 않은 코드가 있다고 하자.

 

function foo() {
  return "no async";
}

const bar = foo();
console.log(bar);  // no async

 

보통의 코드에서는 전혀 문제가 되지 않는다. 하지만 foo에서 처리하는 로직이 시간이 많이 걸리는 작업이라면 문제가 된다. 그 이유는 JavaScript가 싱글 스레드 기반의 언어이기 때문인데, 이러한 특성 때문에 JavaScript는 코드를 동기적으로 처리한다.

위에서 foo가 호출이 되면 foo의 코드 블록이 실행이 되는데 코드 블록 내의 작업이 만약 10초가 걸리는 작업이라면 10초 뒤에 'no async'가 콘솔에 찍히게 된다. 즉, 이 10초 동안 foo호출 밑에 있는 코드는 실행이 되지 않는다.

따라서 이때는 비동기적으로 처리를 해주어야 한다. 그래서 이전 포스팅에서 아래와 같이 Promise를 통해 비동기 처리 하는 법을 배웠다. 

 

function foo() {
  return new Promise((resolve, reject) => {
    resolve('promise');
  });
}

const bar = foo();
bar.then(console.log);  // promise

 

위와 같이 코드를 작성하면 Promise가 10초 동안 기다리지 않고 비동기적으로 작업을 처리하고, then을 통해 원할 때 값을 사용할 수 있다. 

 

async

asyncawait은 이 Promise를 좀 더 간편하고 깔끔하게 사용할 수 있게 도와주는 문법이다. 또한 매우 간단하여 쉽게 익힐 수 있다. 위의 코드를 async를 사용하여 바꾸어보자.

 

async function foo() {
  return 'async';
}

const bar = foo();
bar.then(console.log); // async
console.log(bar); // Promise 객체

 

함수 선언 키워드 function앞에 async를 붙혀 사용한다. 이렇게 async를 사용하면 자동으로 foo안의 작업들을 Promise로 변환 시켜준다. bar를 콘솔에 찍어보면 Promise객체가 나오는 것을 볼 수 있다. 즉 async는 좀 더 간편하게 Promise를 만들어주는 문법이라고 할 수 있다.

화살표 함수에서는 다음과 같이 작성한다.

 

const foo = async () => {
  return 'async';
}

const bar = foo();
bar.then(console.log);  // async
console.log(bar);  // Promise 객체

 

await

await은 비동기 처리를 하는 코드 앞에 작성을 한다. await또한 예시를 통해 살펴보자.

 

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function getWhale() {
  await delay(1000);
  return '🐳';
}

async function getDolphin() {
  await delay(1000);
  return '🐬';
}

 

delay는 ms만큼의 시간이 지나면 resolve를 호출하는 Promise를 리턴한다. 그리고 getWhale이나 getDolphin에서는 await을 통해 delay에 1초의 시간을 줌으로써 1초를 기다린 후 각각 고래와 돌고래를 리턴하는 Promise를 만든다. Promise로 리턴이 되는 것은 앞서 설명한 것과 같이 async를 사용했기 때문에 그렇다.

getWhaleasyncawait을 사용하지 않고 Promise를 통해 작성하면 아래와 같다.

 

function getWhale() {
  return delay(1000).then(() => "🐳");
}

 

다음으로는 고래와 돌고래를 얻어오는 함수인 getAnimalsasyncawait을 통해 만들어보자.

 

async function getAnimals() {
  const whale = await getWhale();
  const dolphin = await getDolphin();
  return `${whale} ${dolphin}`;
}

getAnimals().then(console.log); // 🐳 🐬

 

비동기 처리를 하는 getWhalegetDolphin앞에 await을 통해 각각 1초씩 기다리고, 고래와 돌고래를 리턴하는 Promise를 생성한다. 그리고 then을 통해 콘솔에 찍어보면 2초 뒤에 고래와 돌고래가 나타난다. 이것을 Promise로 똑같이 작성을 하면 아래와 같다.

 

function getAnimals() {
  return getWhale().then(whale => {
    return getDolphin().then(dolphin => `${whale} ${dolphin}`);
  });
}

getAnimals().then(console.log);  // 🐳 🐬

 

이처럼 Promise로 작성할 경우 chaining을 통해 작성을 하게 되는데, 이럴 경우 가독성이 좀 떨어지고 콜백 지옥을 연상시키는 것을 볼 수 있다.

 

try, catch

asyncawait에서의 에러처리는 Promise와 약간 다르다. Promise에서 catch를 통해 에러처리를 했다면 asyncawait에서는 try catch문을 통해 에러처리를 한다. getWhale에서 에러가 발생했다고 가정하고 이를 처리해보자. 먼저 getWhale에서 throw를 통해 에러 내용을 작성하고 getAnimals에서 try catch문을 통해 에러를 처리한다.

 

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getWhale() {
  await delay(1000);
  throw 'Something went wrong!';
  return "🐳";
}

async function getDolphin() {
  await delay(1000);
  return "🐬";
}

async function getAnimals() {
  try {
    const whale = await getWhale();
    const dolphin = await getDolphin();
    return `${whale} ${dolphin}`;
  }
  catch(error) {
    console.log(error); // Something went wrong!
  }
}

getAnimals().then(console.log); // undefined

 

await 병렬 처리

위 코드에서 고래와 돌고래를 가져오는 데에 각 1초씩 총 2초가 걸리는데, 서로 관련이 없는 함수이기 때문에 서로를 기다릴 필요가 없다. 이때 이것을 병렬적으로 처리하여 1초만에 고래와 돌고래를 같이 가져오게 할 수 있다. 첫 번째 방법은 고래와 돌고래의 Promise를 생성하는 방법이다.

 

async function getAnimals() {
  try {
    const whalePromise = getWhale();
    const dolphinPromise = getDolphin();
    const whale = await whalePromise;
    const dolphin = await dolphinPromise;
    return `${whale} ${dolphin}`;
  } catch (error) {
    console.log(error);
  }
}

getAnimals().then(console.log); // 🐳 🐬

 

위와 같이 whalePromisedolphinPromise를 통해 고래와 돌고래의 Promise를 만들었다. Promise를 만들게 되면 만들자마자 자동으로 비동기 작업이 실행되게 때문에 고래와 돌고래를 병렬적으로 얻어올 수 있다. 하지만 이러한 방법은 보는 것과 같이 코드가 지저분해지기 때문에 좋지 않다.

 

두 번째로, 더 깔끔하고 좋은 방법은 Promiseall메서드를 사용하는 것이다.

 

function getAnimals() {
  return Promise.all([getWhale(), getDolphin()]).then((animals) =>
    animals.join(" ")
  );
}

getAnimals().then(console.log); // 🐳 🐬

 

Promiseall메서드에는 Promise배열을 전달하고, 배열 안에 있는 모든 Promise가 병렬적으로 결과를 다 받아올 때 까지 모아주고 다 받아지게 되면 결과 값을 담은 배열을 리턴한다. 첫 번째 방법보다 훨씬 더 간결하고 깔끔하게 작성한 것을 볼 수 있다.

 

추가적으로 Promiserace메서드를 사용하면 Promise들 중에 가장 먼저 값을 받아오는 Promise의 값을 리턴하게 할 수 있다. 

 

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getWhale() {
  await delay(2000);
  return "🐳";
}

async function getDolphin() {
  await delay(1000);
  return "🐬";
}

function getAnimals() {
  return Promise.race([getWhale(), getDolphin()]);
}

getAnimals().then(console.log);  // 🐬

 

위처럼 고래를 받아오는 데는 2초가 걸리고 돌고래를 받아오는 데는 1초가 걸린다고 할 때, race에 고래와 돌고래의 Promise배열을 넘겨주면 1초가 걸리는 돌고래를 리턴하는 것을 볼 수 있다.

 

이전 포스팅까지 해서 JavaScript에서 비동기 처리를 하는 Promiseasync & await을 알아봤다. 네트워크 통신을 하는 등의 비동기 처리를 해야하는 데이터들을 다룰 때 꼭 사용 해야하는 것들이므로 잘 알아두어야 할 것 같다. 

 

 

참고


생강강

,