Promise 와 async/await 가이드: 자바스크립트 비동기를 어떻게 읽어야 할까
Dev

Promise 와 async/await 가이드: 자바스크립트 비동기를 어떻게 읽어야 할까


자바스크립트에서 비동기를 공부할 때 가장 많이 마주치는 단어가 Promiseasync/await입니다. 처음 보면 문법이 쉬워 보이지만, 실제로는 “왜 필요한가”를 모르면 코드가 금방 헷갈립니다.

특히 async/await는 비동기를 동기처럼 보이게 만들어 주기 때문에 읽기 쉬워지지만, 내부 동작까지 똑같이 동기가 되는 것은 아닙니다.

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

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

핵심은 async/await는 Promise 기반 비동기 코드를 더 읽기 쉽게 만드는 문법이고, Promise는 미래에 완료될 값을 표현하는 객체라는 점입니다.

Promise 란 무엇인가

Promise는 “지금은 없지만 나중에는 생길 결과”를 표현하는 자바스크립트 객체입니다.

예를 들어 네트워크 요청은 바로 결과가 나오지 않습니다. 이때 Promise는:

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

를 나타내며, 결과가 준비되면 그다음 처리를 연결할 수 있게 해 줍니다.

callback 만으로는 왜 불편할까

예전에는 비동기 작업 뒤 처리를 callback 으로 많이 연결했습니다.

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

이런 구조는 중첩이 깊어질수록 읽기 어려워집니다. 에러 처리도 분산되기 쉽습니다.

Promise는 이런 흐름을 더 선형적으로 연결하기 쉽게 만들었습니다.

Promise 는 어떻게 읽으면 좋을까

기본적으로 Promise는 .then(), .catch()를 붙여 읽습니다.

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

이 방식은 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/await는 비동기 코드를 동기처럼 “보이게” 해 주는 문법에 가깝습니다.

Promise 와 async/await 중 무엇이 더 좋은가

보통은 async/await가 더 읽기 쉬운 경우가 많습니다. 하지만 Promise 체인이 더 자연스러운 경우도 있습니다.

예를 들어:

  • 순차 흐름이 길다 -> async/await가 읽기 쉬움
  • 여러 Promise를 조합한다 -> Promise.all() 같은 패턴이 더 자연스러울 수 있음

즉, 둘은 대체 관계라기보다, Promise 위에서 async/await가 더 읽기 쉬운 문법을 제공한다고 보는 편이 좋습니다.

자주 하는 실수

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로 바꿔 보기
  4. 독립 작업을 Promise.all()로 묶어 보기

이렇게 비교하면 문법만 외우는 것보다 훨씬 빨리 감이 옵니다.

FAQ

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

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

Q. await 는 아무 데서나 쓸 수 있나요?

보통 async 함수 안에서 씁니다.

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

반대로 생각하는 편이 좋습니다. async/await를 잘 쓰려면 Promise 개념이 바탕에 있어야 합니다.

  • 이런 비동기 흐름이 실제 런타임에서 어떻게 스케줄되는지 궁금하다면 Event Loop 가이드를 이어서 읽어보세요.
  • 먼저 더 큰 개념 차이를 다시 정리하고 싶다면 동기 vs 비동기 가이드를 같이 보는 것도 좋습니다.

먼저 읽어볼 가이드

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