JavaScriptMedium#05
Promise와 async/await의 차이점 및 에러 처리 방법을 설명해주세요.
#JS#비동기#Promise#async/await
힌트
체이닝 방식과 동기 코드처럼 작성하는 방식의 차이를 생각해보세요.
정답 및 해설
Promise와 async/await의 차이점 및 에러 처리 방법을 설명해주세요.
JavaScript에서 비동기 작업을 처리하는 방법은 콜백(Callback)에서 시작해 Promise, 그리고 async/await로 진화해왔습니다. async/await는 Promise 위에 만들어진 **문법적 설탕(syntactic sugar)**으로, 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 해줍니다.
콜백 지옥에서 Promise로
콜백 지옥 (Callback Hell)
// 콜백 방식 — 중첩이 깊어질수록 읽기 어려워짐
getUser(userId, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
getAuthor(comments[0].authorId, (author) => {
console.log(author); // 콜백 지옥
}, handleError);
}, handleError);
}, handleError);
}, handleError);
Promise
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 세 가지 상태를 가집니다.
- Pending: 초기 상태, 아직 완료/실패 전
- Fulfilled: 작업이 성공적으로 완료됨
- Rejected: 작업이 실패함
기본 사용법
const promise = new Promise((resolve, reject) => {
// 비동기 작업 수행
setTimeout(() => {
const success = true;
if (success) {
resolve('데이터 로드 성공'); // Fulfilled
} else {
reject(new Error('데이터 로드 실패')); // Rejected
}
}, 1000);
});
promise
.then((data) => console.log(data)) // 'data 로드 성공'
.catch((err) => console.error(err)) // 에러 처리
.finally(() => console.log('완료')); // 성공/실패 무관하게 실행
Promise 체이닝
function getUser(id) {
return fetch(`/api/users/${id}`).then((res) => res.json());
}
function getPosts(userId) {
return fetch(`/api/posts?userId=${userId}`).then((res) => res.json());
}
getUser(1)
.then((user) => getPosts(user.id)) // Promise를 반환하면 체인이 이어짐
.then((posts) => console.log(posts))
.catch((err) => console.error(err)); // 체인 어디서든 발생한 에러를 잡음
Promise.all / Promise.allSettled / Promise.race
// Promise.all — 모두 성공해야 resolve, 하나라도 실패하면 reject
const [user, posts] = await Promise.all([
fetch('/api/user/1').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
// Promise.allSettled — 성공/실패 여부 무관하게 모든 결과 반환
const results = await Promise.allSettled([
fetch('/api/a').then((r) => r.json()),
Promise.reject(new Error('실패')),
]);
results.forEach((result) => {
if (result.status === 'fulfilled') console.log(result.value);
else console.error(result.reason);
});
// Promise.race — 가장 먼저 완료된 Promise의 결과 반환
const fastest = await Promise.race([
fetch('/api/fast'),
fetch('/api/slow'),
]);
async/await
async 함수는 항상 Promise를 반환합니다. await는 Promise가 settled될 때까지 해당 async 함수의 실행을 일시 중단합니다.
기본 사용법
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`); // Promise가 resolve될 때까지 대기
const user = await response.json();
return user; // Promise<user>로 래핑되어 반환
}
// async 함수는 Promise를 반환하므로 .then()으로 받을 수 있음
fetchUser(1).then((user) => console.log(user));
// 또는 async 컨텍스트 내에서 await
async function main() {
const user = await fetchUser(1);
console.log(user);
}
Promise 체이닝 vs async/await 비교
같은 로직을 두 방식으로 작성한 예시입니다.
// Promise 체이닝
function getUserWithPosts(userId) {
return getUser(userId)
.then((user) => {
return getPosts(user.id).then((posts) => ({ user, posts }));
})
.then(({ user, posts }) => {
return getComments(posts[0].id).then((comments) => ({
user,
posts,
comments,
}));
});
}
// async/await — 훨씬 읽기 쉬움
async function getUserWithPosts(userId) {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return { user, posts, comments };
}
에러 처리
Promise의 에러 처리
fetch('/api/data')
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP 에러: ${res.status}`);
}
return res.json();
})
.then((data) => console.log(data))
.catch((err) => {
// .then() 체인에서 발생한 모든 에러를 잡음
console.error('에러 발생:', err.message);
})
.finally(() => {
// 로딩 스피너 종료 등
setLoading(false);
});
async/await의 에러 처리
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP 에러: ${response.status}`);
}
const data = await response.json();
return data;
} catch (err) {
// await에서 발생한 에러를 잡음
console.error('에러 발생:', err.message);
throw err; // 필요하면 다시 throw
} finally {
setLoading(false);
}
}
세밀한 에러 처리
async function processMultiple() {
// 각 작업별로 다른 에러 처리가 필요한 경우
let user;
try {
user = await getUser(1);
} catch (err) {
console.error('사용자 조회 실패:', err);
return null;
}
let posts;
try {
posts = await getPosts(user.id);
} catch (err) {
console.error('게시글 조회 실패:', err);
posts = []; // 기본값 사용
}
return { user, posts };
}
유틸리티 패턴 — 에러를 튜플로 반환
// Go 언어 스타일 에러 처리 패턴
async function safeAwait(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
async function main() {
const [err, user] = await safeAwait(getUser(1));
if (err) {
console.error('에러:', err);
return;
}
console.log(user);
}
병렬 처리 주의사항
// 나쁜 예 — 순차 실행 (불필요하게 느림)
async function fetchSequential() {
const user = await getUser(1); // 1초 대기
const posts = await getPosts(1); // 1초 대기 (총 2초)
return { user, posts };
}
// 좋은 예 — 병렬 실행
async function fetchParallel() {
const [user, posts] = await Promise.all([
getUser(1), // 동시 시작
getPosts(1), // 동시 시작 (총 1초)
]);
return { user, posts };
}
차이점 정리
| 구분 | Promise | async/await |
|---|---|---|
| 문법 | .then() / .catch() 체이닝 | 동기 코드처럼 작성 |
| 에러 처리 | .catch() | try/catch |
| 가독성 | 체이닝이 길어지면 복잡 | 직관적, 읽기 쉬움 |
| 디버깅 | 스택 트레이스 추적 어려움 | 스택 트레이스 명확 |
| 내부 동작 | - | Promise 기반 |
| 병렬 처리 | Promise.all() | Promise.all() + await |
실무에서는 async/await를 기본으로 사용하고, 병렬 처리가 필요하면 Promise.all()을 await와 함께 사용하는 것이 권장 패턴입니다.