# JavaScript Async/Await 완벽 가이드: 비동기 처리의 정석
Table of Contents
Async/await는 JavaScript에서 비동기 코드를 동기 코드처럼 직관적으로 작성할 수 있게 해주는 혁신적인 문법입니다. Promise를 기반으로 동작하지만, 콜백 지옥(Callback Hell)이나 복잡한 Promise 체이닝보다 훨씬 더 읽기 쉽고 유지보수하기 좋은 코드를 만들 수 있습니다.
Async/Await 기초
async 키워드는 함수를 비동기 함수로 선언하며, await 키워드는 Promise가 처리될 때까지 함수의 실행을 일시 중지합니다.
// 기본 async 함수 구조
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('데이터 가져오기 실패:', error);
throw error;
}
}
// async 함수는 항상 Promise를 반환합니다
fetchData().then((data) => console.log('성공:', data));
:::important 핵심 개념
async 함수는 항상 Promise를 반환합니다. 함수 내부에서 값을 반환하면 자동으로 Promise.resolve(value)로, 에러를 던지면 Promise.reject(error)로 감싸집니다.
:::
Promise vs. Async/Await 비교
동일한 로직을 구현했을 때의 차이를 비교해 보겠습니다.
// [Promise 체이닝]
function getUserDataWithPromise(userId) {
return fetch(`/api/users/${userId}`)
.then((response) => {
if (!response.ok) throw new Error('사용자를 찾을 수 없습니다.');
return response.json();
})
.then((user) => {
return fetch(`/api/posts?userId=${user.id}`);
})
.then((response) => response.json())
.then((posts) => {
return { user, posts };
})
.catch((error) => {
console.error('에러 발생:', error);
throw error;
});
}
// [Async/Await] - 훨씬 더 직관적입니다
async function getUserDataWithAsync(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) throw new Error('사용자를 찾을 수 없습니다.');
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error('에러 발생:', error);
throw error;
}
}
:::tip 가독성 Async/await를 사용하면 비동기 흐름이 위에서 아래로 흐르는 동기 코드처럼 보여, 코드의 의도를 파악하고 디버깅하기가 훨씬 수월해집니다. :::
에러 처리의 정석
Try-Catch 블록 활용
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP 에러! 상태 코드: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
// 네트워크 연결 실패 등의 에러
console.error('네트워크 에러:', error.message);
} else {
console.error('요청 에러:', error.message);
}
throw error; // 호출한 곳으로 에러 전파
}
}
Finally 블록 활용
async function fetchWithLoading(url) {
showLoadingSpinner(); // 로딩 시작
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
showErrorMessage(error.message);
throw error;
} finally {
hideLoadingSpinner(); // 성공하든 실패하든 로딩 종료
}
}
병렬 실행 최적화: Promise.all
여러 비동기 작업이 서로 의존하지 않는다면, Promise.all을 사용하여 병렬로 실행하는 것이 성능상 유리합니다.
// [순차 실행] - 느림 (총 시간 = A + B + C)
async function fetchSequential() {
const user = await fetch('/api/user').then((r) => r.json());
const posts = await fetch('/api/posts').then((r) => r.json());
const comments = await fetch('/api/comments').then((r) => r.json());
return { user, posts, comments };
}
// [병렬 실행] - 빠름 (총 시간 = Max(A, B, C))
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
fetch('/api/comments').then((r) => r.json()),
]);
return { user, posts, comments };
}
:::important 성능 팁
서로 결과가 필요 없는 독립적인 요청들은 반드시 Promise.all로 묶어서 실행하세요. 전체 응답 시간이 획기적으로 줄어듭니다.
:::
실전 패턴: 재시도(Retry) 로직 구현
네트워크 요청은 언제든 실패할 수 있습니다. 지수 백오프(Exponential Backoff)를 적용한 재시도 로직은 필수 패턴입니다.
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
const isLastAttempt = i === retries - 1;
if (isLastAttempt) {
console.error(`${retries}회 시도 실패:`, error);
throw error;
}
console.log(`${i + 1}번째 시도 실패, ${delay}ms 후 재시도...`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2; // 대기 시간을 2배로 늘림 (지수 백오프)
}
}
}
흔한 실수와 해결책
1. forEach 루프에서 await 사용
forEach는 비동기 함수를 기다려주지 않습니다.
// [나쁜 예] - 의도대로 동작하지 않음
async function bad() {
const items = [1, 2, 3];
items.forEach(async (item) => {
await processItem(item);
});
console.log('완료'); // processItem이 끝나기도 전에 출력됨
}
// [좋은 예] - for...of 루프 사용
async function good() {
const items = [1, 2, 3];
for (const item of items) {
await processItem(item); // 하나씩 순차적으로 실행
}
console.log('완료'); // 모든 처리가 끝난 후 출력됨
}
// [좋은 예] - 병렬 처리 (Promise.all)
async function parallel() {
const items = [1, 2, 3];
await Promise.all(items.map((item) => processItem(item)));
console.log('완료');
}
2. 불필요한 await
// [비효율]
async function returnData() {
return await fetch('/api/data'); // 여기서 await는 불필요
}
// [효율]
async function returnData() {
return fetch('/api/data'); // Promise를 그대로 반환해도 됨
}
단, try-catch 블록 내부에서는 에러를 잡기 위해 await가 필수입니다.
정리
Async/await는 모던 자바스크립트 비동기 처리의 표준입니다.
- 가독성: 동기 코드처럼 읽히는 직관적인 구조
- 에러 처리:
try-catch로 표준화된 에러 핸들링 - 성능:
Promise.all을 활용한 병렬 처리 최적화
이 패턴들을 숙지하고 실무에 적용한다면, 훨씬 더 견고하고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다.