JavaScript Async/Await 완벽 가이드: 비동기 처리의 정석
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을 활용한 병렬 처리 최적화
이 패턴들을 숙지하고 실무에 적용한다면, 훨씬 더 견고하고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다.