Skip to content

async / await

자바스크립트에서 비동기 작업을 처리할 때 가장 기본적인 방식은 Promise.then()을 사용하는 것이다. 하지만 .then()이 중첩되거나 체이닝이 길어지면 코드의 흐름을 따라가기 어려워지고, 가독성도 떨어지게 된다. 이러한 문제를 해결하기 위해 도입된 문법이 async와 await이다.

async / await은 비동기 코드를 동기 코드처럼 순차적으로 작성할 수 있도록 해주는 문법이다. 복잡한 비동기 흐름도 간결하게 표현할 수 있어 코드의 가독성과 유지보수성을 크게 높여준다.

  • .then() 체이닝보다 가독성이 좋다
  • 예외 처리도 try-catch로 간단하게 관리할 수 있다
  • 중첩 없이 위에서 아래로 흐르는 구조로 코드를 작성할 수 있다

결과적으로 async / await은 가독성, 예외 처리, 유지보수 측면에서 우수하기 때문에 현재 가장 많이 사용되는 비동기 처리 방식 중 하나이다.


less
async / await
├── async / await
├── try-catch (예외 처리)
└── Promise.all()

async / await

js
async function 함수이름() {
  const result = await 비동기함수();
  return result;
}
js
// Promise와 .then()을 사용한 기본적인 비동기 처리 코드
// 값이 있는 Promise(3초 후 메시지를 반환받아 출력하는 구조)
const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("3초가 지났습니다");
    }, ms);
  });
};

const start = () => {
  delay(3000).then((res) => {
    console.log(res);
  });
};

start(); // 3초가 지났습니다
  • 값을 활용하는 Promise 구조로, 결과값을 활용해야 할 때 적합하다.
  • resolve("3초가 지났습니다")는 문자열을 반환하며, .then(res)의 res는 이 값을 받는다.
js
// Promise와 .then()을 사용한 기본적인 비동기 처리 코드
// 값이 없는 Promise(3초 후 실행만 하는 구조)
const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const start = () => {
  delay(3000).then(() => {
    console.log("대기");
  });
};

start(); // 대기
  • delay 함수는 지연 시간을 인수로 받아 setTimeout을 Promise로 감싼 비동기 함수이다. 일정 시간이 지난 후 resolve()를 호출해 다음 흐름으로 넘어가도록 한다.
  • resolve()에 값을 전달하지 않으면, .then()은 별도의 인자 없이 실행만 한다. 주로 단순 대기 후 작업을 실행할 때 사용하는 구조이다.
  • start 함수는 delay가 끝난 뒤 .then() 메서드를 통해 "대기"라는 메시지를 출력한다.
  • 위 코드는 async/await 문법으로 더 직관적으로 변경할 수 있다.
js
// async만 사용하여 반환값을 .then()으로 처리하는 경우
const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const start = async () => {
  return "대기";
};

start().then((result) => {
  console.log(result);
});

// 대기
  • async는 비동기 함수임을 나타내는 키워드로, 함수 앞에 붙이면 해당 함수는 자동으로 Promise를 반환하는 비동기 함수가 된다.
  • async 함수 내부에서 return한 값은 자동으로 Promise로 감싸져 반환되며, 이는 마치 resolve()를 호출한 것과 같은 효과를 낸다. 이 결과는 .then()을 통해 이어서 처리할 수 있다.
js
// async, await을 함께 사용해, await으로 비동기 흐름을 제어하는 경우
const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const start = async () => {
  await delay(2000); // delay 함수가 처리 완료될 때까지 기다림
  console.log("대기");
};

start();
  • awaitasync 함수 내부에서만 사용할 수 있는 키워드이다.
  • await 앞에 오는 Promise가 처리될 때까지 기다린 뒤, 다음 줄의 코드를 실행한다.
  • 덕분에 비동기 작업도 동기 코드처럼 위에서 아래로 순차적으로 작성할 수 있게 된다.

try-catch

async / await을 사용할 때는 try-catch 문을 활용해 비동기 작업 중 발생할 수 있는 에러를 안전하게 감지하고 처리할 수 있다. 여러 개의 await가 있는 경우, 한 곳에서 에러를 처리할 수 있어 코드가 깔끔해진다. 코드 흐름이 중단되지 않고, 예외가 발생해도 이후 로직을 유연하게 이어갈 수 있다.
이는 Promise의 .catch()와 같은 역할을 하며, 예외 상황을 보다 구조적으로 다룰 수 있게 해준다.

js
async function 작업() {
  try {
    const result = await something();
    console.log(result);
  } catch (err) {
    console.error(err);
  }
}
js
const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const start = async () => {
  try {
    await delay(2000);
    console.log("대기");
  } catch (error) {
    console.log(error);
  }
};

start();
  • try 블록 안의 코드가 실행되며, 이 과정에서 에러가 발생하면 즉시 catch 블록으로 흐름이 넘어간다.
  • 발견된 에러는 catch(error)의 error 객체에 담기며, 이를 통해 에러의 원인을 확인할 수 있다.
js
// async / await을 사용한 비동기 작업의 순차 실행
const workA = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workA");
    }, 5000);
  });
};

const workB = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workB");
    }, 3000);
  });
};

const workC = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workC");
    }, 10000);
  });
};

const start = async () => {
  try {
    let resultA = await workA(); // 5초 대기
    let resultB = await workB(); // 그 다음 3초 대기
    let resultC = await workC(); // 마지막 10초 대기
    console.log(resultA);
    console.log(resultB);
    console.log(resultC);
  } catch (error) {
    console.log(error);
  }
};

start();

// workA
// workB
// workC
  • workA, workB, workC 함수는 각각 5초, 3초, 10초 후에 문자열을 반환하는 비동기 함수이다. 내부에서는 setTimeout()과 Promise를 사용해 일정 시간 뒤 resolve()를 호출한다.
  • start() 함수는 async 함수이며, 내부에서 await을 사용할 수 있다. 각 await는 해당 Promise가 완료될 때까지 다음 줄로 넘어가지 않고 대기한다.
  • 따라서 작업은 순차적으로 실행되며, 전체 실행 시간은 5초 + 3초 + 10초 = 약 18초가 소요된다. 모든 작업이 완료된 후, 18초 뒤에 결과가 차례대로 콘솔에 출력된다.

Promise.all()

Promise.all()은 여러 개의 비동기 작업(Promise)을 병렬로 실행하고, 모든 작업이 성공(Fulfilled)해야 결과를 배열로 반환하는 메서드이다. 전달된 프로미스 중 하나라도 실패(Rejected)하면 전체가 실패로 간주되며, catch 블록으로 흐름이 이동한다.

Promise.all()은 병렬 처리를 통해 전체 실행 시간을 단축할 수 있는 유용한 방법이다. 각 작업의 소요 시간과 상관없이, 가장 오래 걸리는 작업이 끝나는 시점에 모든 결과를 받을 수 있다. 단, 하나라도 실패하면 전체가 실패로 처리되므로 주의가 필요하다.

js
const workA = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workA");
    }, 5000);
  });
};

const workB = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workB");
    }, 3000);
  });
};

const workC = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("workC");
    }, 10000);
  });
};

const start = async () => {
  try {
    let results = await Promise.all([workA(), workB(), workC()]);
    result.forEach((res) => console.log(res));
  } catch (error) {
    console.log(error);
  }
};

start();

// workA
// workB
// workC
  • Promise.all()은 세 개의 작업 workA, workB, workC를 동시에 실행한다.
  • 가장 오래 걸리는 workC가 끝나는 약 10초 후, 세 작업의 결과가 배열에 담겨 반환된다.
  • 결과 배열의 순서는 [ "workA", "workB", "workC" ]로, 작업의 실행 완료 순서가 아닌 호출된 순서를 따른다.
  • 하나라도 실패하면, 해당 에러가 catch 블록으로 전달되고 다른 결과는 무시된다.