async 함수 선언, await로 Promise 순차 처리, try/catch/finally로 오류 처리, 병렬 처리 패턴, 최상위 await, 실전 활용 패턴까지 async·await를 완전히 익힙니다.
🎯 학습 목표
이 강을 마치면 다음을 할 수 있습니다.
async함수와await키워드의 역할을 이해합니다.try/catch/finally로 async 함수에서 오류를 처리할 수 있습니다.await Promise.all()로 병렬 비동기 처리를 구현할 수 있습니다.- 24강의 Promise 체이닝과 async/await를 상황에 맞게 선택할 수 있습니다.
⚡ async/await란
이 섹션에서는 async/await가 무엇인지, Promise와 어떤 관계인지 살펴보겠습니다.
async/await는 ES2017(ES8)에서 도입된 문법으로, Promise를 기반으로 동작하지만 동기 코드처럼 읽힐 수 있게 작성하는 방법입니다. .then() 체이닝 없이 마치 위에서 아래로 순서대로 실행되는 것처럼 보이는 코드를 작성할 수 있습니다. 내부적으로는 Promise를 사용하므로, 24강의 Promise를 이해한 것이 기반이 됩니다.
// async 함수 선언
async function fetchUser(id) {
// await: Promise가 완료될 때까지 기다린 후 결과값 반환
const response = await fetch('/api/users/' + id);
const user = await response.json();
return user; // async 함수는 항상 Promise를 반환
}
// 일반 함수 vs async 함수 반환값 비교
function normalFn() { return 42; } // 42 반환
async function asyncFn() { return 42; } // Promise<42> 반환
console.log(normalFn()); // 42
console.log(asyncFn()); // Promise { 42 }
// async 함수의 결과는 .then()으로 받거나 await로 기다림
asyncFn().then(val => console.log(val)); // 42
const val = await asyncFn(); // 42 (다른 async 함수 안에서)
async 키워드를 함수 앞에 붙이면 그 함수는 항상 Promise를 반환하는 비동기 함수가 됩니다. 함수 안에서 return 42를 하면 Promise.resolve(42)를 반환하는 것과 동일합니다. await는 async 함수 안에서만 사용할 수 있습니다. await 뒤에 Promise를 놓으면, 그 Promise가 완료될 때까지 함수 실행을 일시 중단하고 결과값을 반환합니다. 중요한 점은 await가 함수를 중단하는 동안 이벤트 루프는 차단하지 않습니다. 다른 작업이 계속 실행될 수 있습니다.
🔄 Promise 체이닝 vs async/await 비교
이 섹션에서는 동일한 작업을 두 가지 방식으로 작성해 가독성 차이를 직접 확인해 보겠습니다.
// Promise 체이닝 방식 (24강)
function loadUserData(userId) {
return fetch('/api/users/' + userId)
.then(response => {
if (!response.ok) throw new Error('사용자 조회 실패');
return response.json();
})
.then(user => {
return fetch('/api/orders?userId=' + user.id)
.then(res => res.json())
.then(orders => ({ user, orders }));
})
.then(({ user, orders }) => {
return fetch('/api/products/' + orders[0].productId)
.then(res => res.json())
.then(product => ({ user, orders, product }));
})
.catch(err => console.error(err));
}
// async/await 방식 (훨씬 읽기 쉬움)
async function loadUserData(userId) {
const userRes = await fetch('/api/users/' + userId);
if (!userRes.ok) throw new Error('사용자 조회 실패');
const user = await userRes.json();
const ordersRes = await fetch('/api/orders?userId=' + user.id);
const orders = await ordersRes.json();
const productRes = await fetch('/api/products/' + orders[0].productId);
const product = await productRes.json();
return { user, orders, product };
}
// 호출
loadUserData(1)
.then(data => console.log(data))
.catch(err => console.error(err));
async/await 버전은 마치 동기 코드처럼 읽힙니다. 복잡한 .then() 체이닝 없이 위에서 아래로 자연스럽게 흐릅니다. 특히 중간에 조건 분기(if)나 반복(for)이 필요할 때 async/await가 훨씬 유연합니다. 반면 .then() 체이닝은 간단한 변환을 연결할 때 더 간결할 수 있습니다. 둘 다 Promise이므로 혼용도 가능합니다.
🛡️ try/catch/finally로 오류 처리
이 섹션에서는 async 함수에서 오류를 처리하는 try/catch 패턴을 살펴보겠습니다.
24강의 Promise에서 .catch()로 오류를 처리했다면, async/await에서는 일반 동기 코드와 동일한 try/catch/finally를 사용합니다. 이것이 async/await의 큰 장점 중 하나입니다.
async function getUserData(id) {
try {
const response = await fetch('/api/users/' + id);
if (!response.ok) {
throw new Error('HTTP 오류: ' + response.status);
}
const user = await response.json();
return user;
} catch (error) {
// await에서 발생한 오류를 여기서 잡음
console.error('사용자 조회 실패:', error.message);
return null; // 기본값 반환
} finally {
// 성공/실패 모두 실행
console.log('요청 완료');
hideLoadingSpinner();
}
}
// 사용
const user = await getUserData(1);
if (user) {
console.log(user.name);
}
// 오류 유형별 처리
async function processData() {
try {
const data = await fetchData();
const result = await transformData(data);
return result;
} catch (error) {
if (error.name === 'NetworkError') {
console.error('네트워크 오류 — 재시도 필요');
} else if (error.name === 'ValidationError') {
console.error('데이터 형식 오류:', error.message);
} else {
throw error; // 처리 못한 오류는 다시 throw
}
}
}
try 블록 안의 await에서 오류가 발생하면 즉시 catch 블록으로 이동합니다. finally는 성공/실패 여부와 관계없이 항상 실행되므로 정리 작업에 적합합니다. catch에서 return null처럼 기본값을 반환하면 호출자에서 null 체크로 처리할 수 있고, throw error로 다시 던지면 상위 호출자로 오류를 전파합니다.
⚡ 병렬 처리 — await + Promise.all
이 섹션에서는 async/await에서 여러 작업을 병렬로 실행하는 방법을 살펴보겠습니다.
async/await의 함정 중 하나는 await를 연속으로 사용하면 의도치 않게 순차 실행이 될 수 있다는 것입니다. 서로 독립적인 작업이라면 병렬로 실행해야 더 빠릅니다.
// 순차 실행 — 불필요하게 느림 (각각 기다림)
async function loadSequential() {
const user = await fetchUser(1); // 1초 기다림
const posts = await fetchPosts(1); // 또 1초 기다림
const comments = await fetchComments(1); // 또 1초 기다림
// 총 3초!
return { user, posts, comments };
}
// 병렬 실행 — Promise.all 활용 (훨씬 빠름)
async function loadParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// 총 1초 (가장 느린 요청 시간)
return { user, posts, comments };
}
// 언제 순차, 언제 병렬?
async function loadOrderedData(userId) {
// 사용자 정보 먼저 필요 (다음 요청에 user.id 사용)
const user = await fetchUser(userId); // 순차: user.id가 필요
// user.id를 알았으면 주문과 알림은 동시에
const [orders, notifications] = await Promise.all([
fetchOrders(user.id), // 병렬: 서로 독립적
fetchNotifications(user.id) // 병렬
]);
return { user, orders, notifications };
}
// for await...of — 비동기 반복
async function processItems(ids) {
for (const id of ids) {
const item = await fetchItem(id); // 각 항목 순차 처리
await saveItem(item);
}
// 주의: ids.map(id => fetchItem(id))는 모두 동시에 시작
}
서로 의존성이 없는 비동기 작업은 반드시 Promise.all()로 병렬화하세요. 특히 API 여러 개를 호출해 페이지를 초기화하는 패턴에서 이 차이가 체감됩니다. 순차 실행이 필요한 경우는 이전 결과가 다음 요청의 입력이 될 때입니다. for...of 안에서의 await는 순차 실행이며, forEach에서 await는 동작하지 않습니다 — 반드시 for...of나 for...in을 사용하세요.
🌐 최상위 await (Top-level await)
이 섹션에서는 모듈 최상위 레벨에서 await를 바로 사용하는 최신 기능을 살펴보겠습니다.
ES2022부터 ES 모듈(type="module")에서는 async 함수로 감싸지 않고 최상위 레벨에서 await를 직접 사용할 수 있습니다.
// 기존: async 래퍼 필요
(async () => {
const config = await loadConfig();
app.start(config);
})();
// 최상위 await (ES 모듈에서만, type="module")
const config = await loadConfig();
app.start(config);
// 실전: 설정 파일 로딩, 초기화
const response = await fetch('/api/config');
const settings = await response.json();
console.log('앱 시작:', settings.version);
💡 실전 패턴 모음
이 섹션에서는 자주 마주치는 실전 async/await 패턴을 살펴보겠습니다.
// 패턴 1: 재시도 로직
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.json();
} catch (error) {
lastError = error;
console.log('재시도 ' + (i + 1) + '/' + maxRetries);
await delay(1000 * (i + 1)); // 지수 백오프
}
}
throw lastError;
}
// 패턴 2: 캐싱
const cache = new Map();
async function fetchCached(url) {
if (cache.has(url)) return cache.get(url);
const data = await fetch(url).then(r => r.json());
cache.set(url, data);
return data;
}
// 패턴 3: 동시 요청 수 제한 (concurrency limit)
async function processWithLimit(items, limit, asyncFn) {
const results = [];
for (let i = 0; i < items.length; i += limit) {
const batch = items.slice(i, i + limit);
const batchResults = await Promise.all(batch.map(asyncFn));
results.push(...batchResults);
}
return results;
}
// 100개 항목을 한 번에 5개씩 병렬 처리
const items = Array.from({ length: 100 }, (_, i) => i + 1);
const results = await processWithLimit(items, 5, async (id) => {
return fetch('/api/items/' + id).then(r => r.json());
});
📝 핵심 요약
25강에서 배운 내용을 정리해 보겠습니다.
| 개념 | 설명 |
|---|---|
async function | 항상 Promise를 반환하는 비동기 함수 |
await | Promise 완료까지 일시 중단 (async 내부에서만) |
try/catch | await 오류를 동기 코드처럼 처리 |
await Promise.all() | 독립적 작업 병렬 실행 — 시간 절약 |
| 순차 vs 병렬 | 의존성 있으면 순차, 독립적이면 병렬 |
| forEach + await | 동작 안 함 — for...of를 사용할 것 |
다음 강인 26강 — fetch로 API 호출에서는 실제 서버와 데이터를 주고받는 방법을 배웁니다. fetch API의 GET·POST 요청, 요청 헤더, JSON 전송, 응답 처리까지 — 25강의 async/await와 결합하면 실제 서버와 통신하는 완전한 웹 앱을 만들 수 있습니다!
관련 주제
- async await
- try catch finally
- await Promise.all
- 병렬 비동기 처리
- 최상위 await
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 자바스크립트 기초 30강 — 웹을 움직이는 언어
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
자바스크립트 기초 30강 — 웹을 움직이는 언어
이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
불러오는 중...
