Promise 와 async/await 가이드: JavaScript 비동기 코드를 어떻게 읽을까
Dev
마지막 업데이트

Promise 와 async/await 가이드: JavaScript 비동기 코드를 어떻게 읽을까


JavaScript 비동기를 공부하기 시작하면 Promiseasync/await를 아주 빨리 만나게 됩니다. 문법만 보면 생각보다 친숙해 보여서 금방 익힐 수 있을 것 같지만, 왜 이 도구들이 필요한지 이해하지 못하면 코드는 다시 금방 헷갈려집니다.

특히 async/await는 비동기 코드를 마치 순차 코드처럼 보이게 만들기 때문에 읽기는 쉬워지지만, 그렇다고 내부 동작까지 진짜 동기 코드로 바뀌는 것은 아닙니다.

이 글에서는 아래 내용을 정리합니다.

  • Promise가 무엇인지
  • async/await가 왜 필요한지
  • callback과 어떻게 다른지

핵심은 Promise는 “나중에 완료될 값”을 표현하는 객체이고, async/await는 그 Promise 기반 비동기 코드를 사람이 읽기 쉽게 풀어 주는 문법이라는 점입니다.

Promise 는 무엇인가

Promise는 지금 당장 결과가 없지만, 나중에는 결과가 생길 작업을 표현하는 JavaScript 객체입니다.

예를 들어 네트워크 요청은 즉시 데이터를 돌려주지 않습니다. 이때 Promise는 작업이:

  • 아직 기다리는 중인지
  • 성공했는지
  • 실패했는지

를 표현하고, 결과가 준비되면 그다음 처리 로직을 연결할 수 있게 해 줍니다.

즉 Promise는 “미래의 결과를 다루는 표준 인터페이스”라고 보면 이해하기 쉽습니다.

callback 만으로는 왜 불편했을까

예전에는 비동기 후속 처리를 callback으로 많이 작성했습니다.

fetchUser(userId, function (user) {
  fetchOrders(user.id, function (orders) {
    saveLog(orders, function () {
      console.log('done');
    });
  });
});

작은 예제에서는 괜찮아 보이지만, 흐름이 길어질수록 들여쓰기가 깊어지고, 에러 처리가 흩어지고, 중간 로직을 재사용하기도 어려워집니다.

Promise는 이 문제를 완전히 없애지는 못해도, 적어도 후속 처리 흐름을 더 구조적으로 연결할 수 있게 만들어 줬습니다.

Promise 체인은 어떻게 읽으면 될까

가장 기본적인 Promise 사용 방식은 .then(), .catch(), 필요하다면 .finally()를 이어 붙이는 것입니다.

fetchUser(userId)
  .then((user) => fetchOrders(user.id))
  .then((orders) => saveLog(orders))
  .catch((error) => console.error(error))
  .finally(() => console.log('finished'));

이 방식은 중첩 callback보다 읽기 좋지만, 체인이 길어지면 여전히 눈으로 따라가기가 피곤해질 수 있습니다.

async/await 는 왜 등장했을까

async/await는 Promise 기반 비동기 로직을 위에서 아래로 읽히는 순차 코드처럼 보이게 만들어 줍니다.

async function run() {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    await saveLog(orders);
  } catch (error) {
    console.error(error);
  }
}

이 구조의 장점은 흐름을 따라가기가 쉽다는 점입니다. “먼저 사용자 정보를 가져오고, 그다음 주문을 가져오고, 마지막에 로그를 저장한다”라는 순서를 머릿속에서 더 자연스럽게 읽을 수 있습니다.

그래서 실무에서는 긴 비동기 로직을 설명하거나 유지보수할 때 async/await를 많이 씁니다.

async/await 가 코드를 동기로 바꾸는 걸까

그렇지는 않습니다. 이 부분이 아주 중요합니다.

await는 Promise 결과가 나올 때까지 그 async 함수 내부 흐름을 잠시 멈춘 것처럼 보이게 만들 뿐입니다. 내부 메커니즘은 여전히 Promise 기반 비동기 구조 위에서 동작합니다.

async/await는 비동기 동작을 동기 코드처럼 “읽히게” 해 주는 문법이지, 비동기를 진짜 blocking 코드로 바꾸는 마법은 아닙니다.

Promise 와 async/await 중 무엇을 써야 할까

대부분의 일상적인 순차 흐름에서는 async/await가 읽기 쉽습니다. 하지만 Promise 자체의 도구들도 여전히 중요합니다.

예를 들어:

  • 순차 단계가 긴 흐름: async/await가 더 읽기 쉬운 경우가 많음
  • 여러 비동기 작업을 함께 조합할 때: Promise.all() 같은 유틸리티가 자연스러움
  • 간단한 후속 처리 연결: .then() 체인이 더 짧을 수도 있음

즉 둘은 경쟁 관계가 아니라 같은 기반 위의 서로 다른 표현 방식에 가깝습니다.

자주 하는 실수

1. await 를 많이 쓰면 성능이 좋아진다고 생각한다

await는 가독성과 제어 흐름을 위한 도구입니다. 자동 성능 최적화 도구는 아닙니다.

2. 독립적인 작업도 순서대로 await 한다

서로 의존하지 않는 작업이라면 병렬로 묶는 편이 더 나을 수 있습니다.

const [user, posts] = await Promise.all([
  fetchUser(userId),
  fetchPosts(userId),
]);

3. 에러 처리를 빼먹는다

Promise 체인이든 async/await든, 실패 경로를 설계하지 않으면 실제 서비스에서는 금방 문제가 됩니다.

읽는 연습을 어떻게 하면 좋을까

처음에는 문법을 외우기보다 같은 흐름을 세 가지 형태로 비교해 보는 것이 좋습니다.

  1. callback 버전
  2. Promise 체인 버전
  3. async/await 버전

이렇게 바꿔 보면 문법보다 흐름이 먼저 눈에 들어옵니다. 그리고 그다음에 Promise.all()처럼 독립 작업을 묶는 패턴까지 보면 훨씬 빠르게 감이 옵니다.

FAQ

Q. async 함수는 항상 Promise 를 반환하나요

그렇습니다. 내부에서 일반 값을 반환해도 Promise로 감싸져 반환됩니다.

Q. await 는 어디에서나 쓸 수 있나요

보통은 async 함수 안에서 사용합니다.

Q. Promise 를 알면 async/await 는 안 배워도 되나요

같이 보는 편이 좋습니다. async/await를 제대로 이해하려면 Promise 개념이 바탕에 있어야 합니다.

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.

광고