JavaScript async와 await

seul_velog·2021년 12월 29일
0

JavaScript

목록 보기
24/25
post-thumbnail

async & await 란?

기존에 존재하는 promise 위에 조금 더 간편한 API를 제공하는 문법적 설탕(syntactic sugar)이다.

  • 깔끔하게 프로미스를 사용할 수 있는 방법이다.
  • 무조건 프로미스 대신 async와 await으로 대체해서 사용해야 하는 것은 아니며, 경우에 따라 다르다.




async

async 함수와 특징

//ex.1) result가 1인 프로미스가 반환된다.
async function f() {
  return 1;
}

f().then(alert); // 1



// ex.2) 명시적으로 프로미스를 반환한다. 결과는 같다.
async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1
  • async는 function 앞에 위치한다.
  • function 앞에 async를 붙이면 해당 함수는 항상 promise를 반환한다.
  • promise가 아닌 값을 반환하더라도 이행 상태의 프로미스(resolved promise)로 값을 감싸 이행된 프로미스가 반환되도록 한다.
  • 즉, async가 붙은 함수는 반드시 프로미스를 반환하고, 프로미스가 아닌 것은 프로미스로 감싸 반환한다.

✍️ 예제를 통해 알아보자.

function fetchUser() {
    return new Promise((resolve, reject) => {
     resolve('seul');
    })
}

const user = fetchUser();  
user.then(console.log);
// suel
  • fetchUser() 는 프로미스를 리턴한다.
  • 위 예제에서 async 키워드를 사용해보자. ▼

async function fetchUser1() { 
    return 'seul!';
}

const user1 = fetchUser1();  
user1.then(console.log); 
// suel!
  • 함수 앞에 async 키워드를 붙이면 자동적으로 함수 안에 있는 블럭들이 promise로 변환되어진다.




await

await 문법과 특징

//await 문법
let value = await promise;
//async와 await
async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프로미스가 이행될 때까지 기다린다.
// 프라미스 객체의 result 값이 변수 result에 할당된다.
  
  alert(result); // "완료!" (1초 뒤에 실행)
}

f();
  • await는 async 함수 안에서만 동작한다.
  • 일반 함수엔 await을 사용할 수 없다. 사용할 경우, 문법 에러가 발생한다.
  • 자바스크립트는 await 키워드를 만나면 프로미스가 처리될 때까지 기다린다. 결과는 그 이후 반환된다.
  • await는 말 그대로 프로미스가 처리될 때까지 함수 실행을 기다리게 만들고, 처리되면 그 결과와 함께 실행이 재개된다.
  • 프로미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.

✍️ 예제를 통해 알아보자.

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

async function getApple(){
    await delay(1000);
    return '🍎';
}

async function getBanana() { 
    await delay(2000);
    return '🍌';
}

getApple().then(console.log); // 1초 후 사과 출력
getBanana().then(console.log); // 2초 후 바나나 출력
  • await는 async가 붙은 함수 안에서만 사용가능하다.
  • delay 라는 함수는 정해진 ms가 지나면 resolve를 호출하는 promise를 리턴한다.
  • await 키워드를 쓰게되면 delay가 끝날 때까지 기다려준다. 그래서 1초 후에 사과를 리턴하는 프로미스가 만들어진다. (async 라는 키워드가 있으므로)
  • 바나나도 다시 프로미스를 만드는 함수이다. 2초 후에 바나나를 리턴한다.

✍️ 사과와 바나나를 한번에 다 가져오는 pickFruits 이라는 함수를 만들어 보자.

(1) 기존의 promise 체이닝을 사용할 경우 ▼


function pickFruits() {
  return getApple()
  .then((apple) => {
    return getBanana()
    .then((banana) => `${apple} + ${banana}`)

pickFruits().then(console.log);
// 🍎 + 🍌 (3초 후에 출력된다.)
  • promise도 중첩적으로 체이닝을 하게되면 콜백지옥과 비슷한 문제점이 생길 수 있다.

(2) async 키워드를 이용하기 ▼

async function pickFruits() {
    const apple = await getApple();
    const banana = await getBanana();
    return `${apple} + ${banana}`;
}

pickFruits().then(console.log);
// 🍎 + 🍌 (3초 후에 출력된다.)
  • 사과와 바나나를 다 가져오고, 그 이후에 리턴한다.
  • 마치 동기적으로 코드를 작성하는 것처럼 자연스럽고 가독성도 좋아졌다.



await을 사용할 때 주의할 점

  • 최상위 레벨 코드(top-level code)에서 작동하지 않는다. 최상위 레벨 코드에서 사용하면 문법 에러가 발생한다.
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
  • 익명 async 함수로 코드를 감싸면 최상위 레벨 코드에도 await를 사용할 수 있다고 한다.🤔
(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();



에러 핸들링

  • 프로미스가 정상적으로 이행되면 await promise 는 프로미스 객체의 result에 저장된 값을 반환한다.
  • 반면 프로미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져진다.
  • throw문은 사용자 정의 예외를 발생(throw)할 수 있다. MDN-throw
async function f() {
  await Promise.reject(new Error("에러 발생!"));
}

// 위와 동일하다.
async function f() {
  throw new Error("에러 발생!");
}
  • try..catch 를 이용한다.
async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

위에서 살펴본 🍎 + 🍌 예제도 확인해보자.

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

async function getApple(){
    await delay(1000);
    throw 'error' // 에러가 발생한다.
    return '🍎';
}

async function getBanana() { 
    await delay(2000);
    return '🍌';
}

async function pickFruits() {
  try {
    const apple = await getApple();
    const banana = await getBanana();
  } catch(err) {
    alert(err);    // 에러 팝업창이 뜬다.
  }
    return `${apple} + ${banana}`;
}

pickFruits().then(console.log);



병렬 처리

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

async function getApple(){
  await delay(2000);
  return '🍎';
}

async function getBanana() { 
  await delay(2000);
  return '🍌';
}

async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);
// 🍎 + 🍌 (4초 후에 출력된다.)
  • 사과를 받아오는 데 2초, 그 다음에 바나나를 받아오는 데 2초, 총 4초 후에 실행이 된다.
  • 두 개가 연관되지 않기 때문에 서로 기다릴 필요가 없으므로 비효율적이다.

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

async function getApple(){
  await delay(2000);
  return '🍎';
}

async function getBanana() { 
  await delay(2000);
  return '🍌';
}

async function pickFruits() {
  const applePromise = getApple();
  const bananaPromise = getBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log);
// 🍎 + 🍌 (2초 후에 출력된다.)
  • applePromise와 bananaPromise를 만들었기 때문에 만들자마자 각각 코드가 실행된다. 즉, 병렬적으로 동시에 실행된다.



Promise의 API인 all()과 race()

(1) Promise.all()

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

async function getApple(){
  await delay(1000);
  return '🍎';
}

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


function pickAllFruits(){
    return Promise.all([getApple(), getBanana()])
      .then(fruits =>
        fruits.join(' + ')
    );
}

pickAllFruits().then(console.log);
// 🍎 + 🍌
  • all() API는 프로미스 배열을 전달하게 되면, 모든 프로미스들이 병렬적으로 다 받을 때까지 모아준다.
  • getApple프로미스와 getBanana프로미스의 배열을 전달한다. → 다 전달되면 then 을 통해 즉, 다 받아진 배열이 다시 전달된다.
  • 배열을 스트링으로 묶는 join() 을 사용해서 출력해 본다.

(2) Promise.race()

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

async function getApple(){
  await delay(3000); // 3초가 소요된다.
  return '🍎';
}

async function getBanana() { 
  await delay(1000); // 1초가 소요된다.
  return '🍌';
}


function pickOnlyOne(){
    return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log);
// 🍌 (바나나가 먼저 가져와진다.)
  • race() API는 배열에 전달된 프로미스 중에서 가장 먼저 값을 리턴하는 것만 전달된다.



📌 check!

function 앞에 async 키워드를 추가하면 두가지 효과가 있다.

  • 함수는 언제나 promise를 반환한다.
  • 함수 안에서 await을 사용할 수 있다.



promise 앞에 await 키워드를 붙이면 자바스크립트는 promise가 처리될 때까지 대기한다. 처리가 완료되면 조건에 따라 아래와 같은 동작이 이어진다.

  • 에러 발생시 - 예외가 생성된다. (에러가 발생한 지점에서 throw error를 호출한 것과 동일하다.)
  • 에러 미발생시 - promise 객체의 result 값을 반환한다.



async & await 그리고 then & catch

  • async & await을 사용하면 await가 대기를 처리해주므로 then을 쓰지 않아도 되고, catch 대신 try..catch를 사용할 수 있다는 장점도 생긴다.

  • 보통은 then을 사용하는 것보다 async & await를 사용하는 것이 더 편리하다고 한다.

  • 하지만 async함수 바깥의 최상위 레벨 코드에선 await를 사용할 수 없다. 그렇기 때문에 관행처럼 then & catch를 추가해 최종 결과나 처리되지 못한 에러를 다룬다고 한다. 🤔




reference
MDN-async
MDN-await
async_await
dream-coding
MDN-all

profile
기억보단 기록을 ✨

0개의 댓글