JavaScript를 다루다 보면 async/await를 만나게 된다. 특히 서버 통신, 파일 처리, 데이터베이스 조회 등 시간이 걸리는 작업을 할 때 거의 필수적으로 사용된다. 다만 async/await가 정확히 뭔지, Promise와는 뭐가 다른지, 언제 사용해야 하는지 명확하게 이해하는 개발자는 생각보다 적다. 그냥 "좋다고 해서" 또는 "이 코드를 봤으니까" 복사해서 쓰는 경우가 많다.
async/await를 제대로 이해하면 복잡한 비동기 코드를 간결하게 작성할 수 있고, 에러 처리도 더 명확해진다. 뿐만 아니라 성능 최적화나 동시 실행 같은 고급 기법도 가능해진다. 이번 글에서는 Promise부터 시작해서 async/await까지 차근차근 설명하고, 실무 예제와 함께 흔한 실수들을 다루겠다.
먼저 왜 async/await가 필요한지 이해해야 한다.
// 동기 처리 - 한 줄씩 순서대로 실행
console.log('시작');
var data = fetch('https://api.example.com/data'); // 3초 걸림
console.log(data);
console.log('끝');
// 문제: 3초 동안 다른 코드가 실행되지 않음 (브라우저 멈춤)
이런 문제를 해결하기 위해 JavaScript는 비동기 처리 방식을 제공한다.
가장 오래된 방식이다. 하지만 복잡하고 읽기 어렵다.
// 콜백 사용
function fetchData(callback) {
setTimeout(function() {
callback('데이터');
}, 2000);
}
fetchData(function(data) {
console.log(data);
});
// 문제: 콜백이 여러 개 중첩되면 "콜백 지옥" 발생
fetchData(function(data1) {
console.log(data1);
fetchData(function(data2) {
console.log(data2);
fetchData(function(data3) {
console.log(data3);
// ... 깊어질수록 읽기 어려움
});
});
});
ES6에서 등장한 Promise는 비동기 작업을 더 깔끔하게 처리한다.
Promise의 기본 구조:
// Promise 생성
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('성공!'); // 성공
// reject('실패!'); // 또는 실패
}, 2000);
});
// Promise 처리
promise
.then(function(result) {
console.log(result); // '성공!' 출력
})
.catch(function(error) {
console.log(error); // 에러 처리
})
.finally(function() {
console.log('완료'); // 성공/실패 상관없이 항상 실행
});
Promise 체이닝 (여러 작업을 순서대로):
fetch('/api/user/1')
.then(response => response.json())
.then(user => {
console.log(user);
return fetch('/api/posts/' + user.id); // 다음 요청
})
.then(response => response.json())
.then(posts => {
console.log(posts);
})
.catch(error => {
console.log('에러:', error);
});
Promise도 좋지만, .then()이 계속 체이닝되면 여전히 읽기 어렵다.
async/await는 Promise를 기반으로 하지만, 동기 코드처럼 보이게 작성할 수 있다.
기본 문법:
// async 함수는 항상 Promise를 반환
async function fetchUser() {
return '사용자 데이터';
}
// await는 async 함수 내에서만 사용 가능
async function getUserData() {
var user = await fetch('/api/user/1'); // Promise 대기
console.log(user);
}
getUserData();
Promise 체이닝을 async/await로 변환:
// Promise 체이닝 방식 (복잡)
fetch('/api/user/1')
.then(response => response.json())
.then(user => {
console.log(user);
return fetch('/api/posts/' + user.id);
})
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.log(error));
// async/await 방식 (간결)
async function getUserAndPosts() {
var user = await fetch('/api/user/1').then(r => r.json());
console.log(user);
var posts = await fetch('/api/posts/' + user.id).then(r => r.json());
console.log(posts);
}
getUserAndPosts();
훨씬 읽기 쉽고 이해하기 쉬워진다!
try-catch 사용 (권장):
async function getData() {
try {
var response = await fetch('/api/data');
var data = await response.json();
console.log(data);
return data;
} catch(error) {
console.log('에러 발생:', error);
} finally {
console.log('완료');
}
}
getData();
Promise의 catch() 사용:
async function getData() {
var response = await fetch('/api/data');
var data = await response.json();
console.log(data);
}
// 함수를 호출할 때 catch 붙이기
getData()
.catch(error => {
console.log('에러:', error);
});
예제 1: 사용자 정보 조회 및 저장
async function getUserAndSave(userId) {
try {
// 1. 사용자 정보 조회
var response = await fetch('/api/user/' + userId);
var user = await response.json();
console.log('사용자:', user);
// 2. 데이터베이스에 저장 (PHP 서버로 POST)
var saveResponse = await fetch('/api/user/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
});
var result = await saveResponse.json();
console.log('저장 결과:', result);
} catch(error) {
console.log('에러:', error.message);
}
}
getUserAndSave(123);
예제 2: 여러 API 요청을 동시에 처리
// ✗ 잘못된 방법 (순차 처리 - 느림)
async function getMultipleData() {
var user = await fetch('/api/user/1').then(r => r.json()); // 1초
var posts = await fetch('/api/posts/1').then(r => r.json()); // 1초
var comments = await fetch('/api/comments/1').then(r => r.json()); // 1초
// 총 3초 걸림
}
// ✓ 올바른 방법 (동시 처리 - 빠름)
async function getMultipleData() {
var [user, posts, comments] = await Promise.all([
fetch('/api/user/1').then(r => r.json()),
fetch('/api/posts/1').then(r => r.json()),
fetch('/api/comments/1').then(r => r.json())
]);
// 총 1초 걸림 (병렬 처리)
}
getMultipleData();
예제 3: 파일 업로드 진행률 표시
async function uploadFile(file) {
try {
var formData = new FormData();
formData.append('file', file);
var response = await fetch('/upload.php', {
method: 'POST',
body: formData
});
// 응답이 올 때까지 대기
var result = await response.json();
if(result.success) {
console.log('업로드 완료:', result.filename);
} else {
throw new Error(result.message);
}
} catch(error) {
console.log('업로드 실패:', error.message);
}
}
// 파일 선택 시 호출
document.getElementById('fileInput').addEventListener('change', function(e) {
uploadFile(e.target.files[0]);
});
실수 1: await를 붙이지 않음
// ✗ 잘못됨
async function getData() {
var data = fetch('/api/data').then(r => r.json()); // Promise 객체가 저장됨
console.log(data); // Promise {...} 출력
}
// ✓ 올바름
async function getData() {
var data = await fetch('/api/data').then(r => r.json());
console.log(data); // 실제 데이터 출력
}
실수 2: async 함수 없이 await 사용
// ✗ 에러 발생
function getData() {
var data = await fetch('/api/data'); // SyntaxError: await 사용 불가
}
// ✓ async 키워드 필수
async function getData() {
var data = await fetch('/api/data');
}
실수 3: 에러 처리 생략
// ✗ 에러 발생 시 프로그램이 중단될 수 있음
async function getData() {
var data = await fetch('/api/data').then(r => r.json());
return data;
}
// ✓ try-catch로 에러 처리
async function getData() {
try {
var data = await fetch('/api/data').then(r => r.json());
return data;
} catch(error) {
console.log('에러:', error);
return null;
}
}
실수 4: 동시 처리 기회를 놓침
// ✗ 순차 처리로 느림 (3초 소요)
async function getDataSequentially() {
var a = await fetch('/api/a').then(r => r.json()); // 1초
var b = await fetch('/api/b').then(r => r.json()); // 1초
var c = await fetch('/api/c').then(r => r.json()); // 1초
return {a, b, c};
}
// ✓ 병렬 처리로 빠름 (1초 소요)
async function getDataInParallel() {
var [a, b, c] = await Promise.all([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json()),
fetch('/api/c').then(r => r.json())
]);
return {a, b, c};
}
Promise.all() - 모든 Promise가 완료될 때까지 대기:
async function waitForAll() {
try {
var results = await Promise.all([
fetch('/api/1').then(r => r.json()),
fetch('/api/2').then(r => r.json()),
fetch('/api/3').then(r => r.json())
]);
console.log(results); // 모두 완료 후 배열로 반환
} catch(error) {
// 하나라도 실패하면 여기서 에러 처리
console.log('에러:', error);
}
}
Promise.race() - 가장 먼저 완료된 것 하나만 반환:
async function waitForFirst() {
var firstResult = await Promise.race([
fetch('/api/1').then(r => r.json()),
fetch('/api/2').then(r => r.json()),
fetch('/api/3').then(r => r.json())
]);
console.log(firstResult); // 가장 빠른 응답만 출력
}
// 용도: 타임아웃 설정
async function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url).then(r => r.json()),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('타임아웃')), timeout)
)
]);
}
| 기준 | async/await | Promise |
|---|---|---|
| 가독성 | ⭐⭐⭐⭐⭐ (우수) | ⭐⭐⭐ (중간) |
| 에러 처리 | try-catch로 명확 | .catch() 사용 |
| 성능 | 동일 | 동일 |
| 복잡한 흐름 제어 | 조금 더 복잡 | 체이닝으로 직관적 |
결론: 대부분의 경우 async/await를 권장한다. 다만 복잡한 흐름 제어가 필요할 때는 Promise 체이닝이 더 나을 수 있다.
async/await는 JavaScript의 비동기 처리를 혁신적으로 단순화했다. 이제 복잡한 비동기 코드도 동기 코드처럼 읽고 쓸 수 있게 되었다. 다만 기본 원리인 Promise를 이해하는 것이 중요하고, 에러 처리와 성능 최적화(동시 처리)를 항상 염두에 두어야 한다. 이 글의 예제들을 직접 실행해보고 응용해보면, async/await를 완벽하게 마스터할 수 있을 것이다.