알아두면 좋은 JavaScript async/await 패턴

Frontend2026-09-20TryQuickToolBox

비동기 프로그래밍은 현대 JavaScript의 핵심 부분이며, async/await 덕분에 훨씬 읽기 쉬워졌습니다. 하지만 많은 개발자들이 여전히 흔한 함정에 빠집니다: 처리되지 않은 promise 거부, 느린 순차 요청, 혹은 혼란스러운 에러 처리 등입니다. 이 글에서는 더 깔끔하고 효율적인 코드를 작성하기 위해 즉시 적용할 수 있는 실용적인 async/await 패턴을 살펴보겠습니다.

1. 기본 async/await 문법

async 함수는 항상 Promise를 반환합니다. await 키워드는 Promise가 처리될 때까지 실행을 일시 중지한 후, 해결된 값을 반환합니다. Promise가 거부되면 try/catch로 잡을 수 있는 에러를 던집니다.

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
  }
}

이것이 기본입니다. 하지만 실제 시나리오에서는 더 세밀한 접근이 필요한 경우가 많습니다.

2. 우아하게 에러 처리하기

async/await의 가장 큰 장점 중 하나는 동기 및 비동기 에러 모두에 try/catch를 사용할 수 있다는 것입니다. 하지만 하나의 try 블록에 여러 await 호출이 있을 때는 주의하세요. 첫 번째가 실패하면 이후 호출은 실행되지 않습니다.

더 세밀한 제어를 위해 각 await를 자체 try/catch로 감싸거나, 반복적인 try/catch 블록을 피하기 위해 헬퍼 함수를 사용할 수 있습니다.

async function getUser(id) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error;
  }
}

3. Promise를 병렬로 실행하기

여러 독립적인 Promise를 순차적으로 await하는 것은 흔한 성능 실수입니다. 여러 소스에서 결과가 필요하다면 Promise.all()을 사용해 동시에 실행하세요.

// Sequential: slow
const user = await fetchUser();
const posts = await fetchPosts();

// Parallel: fast
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

모든 Promise가 해결되거나 거부되든 상관없이 모두 처리될 때까지 기다리려면 Promise.allSettled()를 사용하세요. 일부가 실패해도 결과를 수집하고 싶을 때 유용합니다.

4. 순서가 중요할 때의 순차 실행

각 단계가 이전 단계에 의존하는 경우처럼 항목을 순서대로 처리해야 할 때가 있습니다. 이럴 때는 forEach 대신 for...of 루프와 await를 사용하세요 (forEach는 비동기 콜백을 기다리지 않습니다).

for (const item of items) {
  await processItem(item);
}

동시성을 제한해야 한다면 p-limit 같은 라이브러리를 고려하거나 간단한 큐를 구현하세요.

5. 루프에서 async/await 함정 피하기

Array.prototype.forEach 안에서 await를 사용하는 것은 전형적인 실수입니다. forEach는 비동기 콜백을 기다리지 않으므로, Promise가 해결되기 전에 루프가 끝납니다. 항상 for...of나 Promise.all과 함께 map을 사용하세요.

6. 타임아웃과 함께 쓰는 async/await

요청이 멈추는 것을 방지하기 위해 Promise를 타임아웃과 경쟁시킬 수 있습니다.

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
}

await Promise.race([fetchData(), timeout(5000)]);

이 패턴은 멈출 수 있는 API 호출에 유용합니다.

7. Async 제너레이터와 for await...of

Async 제너레이터를 사용하면 값을 비동기적으로 스트림처럼 생성할 수 있습니다. for await...of로 소비할 수 있습니다.

async function* fetchPages() {
  let page = 1;
  while (true) {
    const data = await fetch(`/api/items?page=${page}`);
    if (data.length === 0) break;
    yield data;
    page++;
  }
}

for await (const page of fetchPages()) {
  console.log(page);
}

페이지네이션 API나 스트리밍 데이터에 아주 좋습니다.

8. 흔한 실수와 피하는 방법

9. 깔끔한 비동기 코드를 위한 모범 사례

FAQ

async/await와 Promise의 차이점은 무엇인가요?

async/await는 Promise에 대한 문법적 설탕으로, 비동기 코드를 동기 코드처럼 보이게 합니다. 체이닝과 try/catch를 통한 에러 처리를 단순화합니다.

forEach 루프 안에서 await를 사용할 수 있나요?

아니요, forEach는 비동기 콜백을 기다리지 않습니다. 대신 for...of나 map과 함께 Promise.all을 사용하세요.

여러 비동기 작업을 병렬로 실행하려면 어떻게 하나요?

Promise.all()을 사용해 동시에 실행하고 모두 해결될 때까지 기다리세요.

JSON API를 다룰 때는 JSON Formatter를 사용해 응답을 빠르게 포맷하고 검증할 수 있습니다.