Promise
프로미스
비동기 작업의 최종 완료/실패를 나타내는 객체. then, catch, finally로 처리. async/await의 기반.
프로미스
비동기 작업의 최종 완료/실패를 나타내는 객체. then, catch, finally로 처리. async/await의 기반.
Promise는 JavaScript에서 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타내는 객체입니다. ES6(ECMAScript 2015)에서 정식으로 도입되어, 콜백 기반 비동기 처리의 복잡성을 해결하고 더 직관적인 비동기 프로그래밍을 가능하게 했습니다. Promise는 "미래의 어떤 시점에 결과를 제공하겠다"는 약속을 의미합니다.
Promise는 세 가지 상태(state)를 가집니다. pending(대기)은 초기 상태로 아직 이행도 거부도 되지 않은 상태입니다. fulfilled(이행)는 작업이 성공적으로 완료된 상태이며, rejected(거부)는 작업이 실패한 상태입니다. 한 번 fulfilled나 rejected가 되면 상태가 변경되지 않으며, 이를 settled(처리됨)라고 합니다.
Promise의 핵심 강점은 체이닝(chaining)입니다. then() 메서드는 새로운 Promise를 반환하므로 .then().then().catch() 형태로 연속된 비동기 작업을 순차적으로 처리할 수 있습니다. catch()로 에러를 한 곳에서 처리하고, finally()로 성공/실패와 관계없이 실행할 코드를 정의할 수 있어 콜백 지옥을 해결합니다.
ES2017에서 도입된 async/await은 Promise를 더 동기 코드처럼 작성할 수 있게 해주는 문법적 설탕(syntactic sugar)입니다. async 함수는 항상 Promise를 반환하고, await 키워드는 Promise가 settled될 때까지 실행을 일시 중지합니다. 내부적으로 여전히 Promise를 사용하므로, Promise의 동작 원리를 이해하는 것이 async/await을 올바르게 사용하는 기초가 됩니다.
// 1. Promise 생성하기
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: '홍길동', email: 'hong@example.com' });
} else {
reject(new Error('유효하지 않은 사용자 ID'));
}
}, 1000);
});
}
// 2. Promise 체이닝 (then, catch, finally)
fetchUser(1)
.then(user => {
console.log('사용자 조회 성공:', user.name);
return fetchUserPosts(user.id); // 다음 Promise 반환
})
.then(posts => {
console.log('게시물 수:', posts.length);
return posts.map(p => p.title);
})
.then(titles => {
console.log('게시물 제목들:', titles);
})
.catch(error => {
// 체인 어디서든 발생한 에러를 여기서 처리
console.error('오류 발생:', error.message);
})
.finally(() => {
console.log('작업 완료 (성공/실패 무관)');
});
// 3. Promise.all - 여러 Promise 병렬 실행
async function fetchDashboardData() {
const [user, posts, notifications] = await Promise.all([
fetchUser(1),
fetchUserPosts(1),
fetchNotifications(1)
]);
// 세 요청이 동시에 시작되어 가장 느린 요청 시간만 소요
console.log('대시보드 데이터:', { user, posts, notifications });
}
// 4. Promise.allSettled - 실패해도 모든 결과 받기
const promises = [
fetchUser(1), // 성공
fetchUser(-1), // 실패
fetchUser(2) // 성공
];
Promise.allSettled(promises).then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${i}: 성공`, result.value);
} else {
console.log(`Promise ${i}: 실패`, result.reason.message);
}
});
});
// 5. async/await으로 동기 코드처럼 작성
async function processUser(userId) {
try {
const user = await fetchUser(userId);
console.log('사용자:', user.name);
const posts = await fetchUserPosts(user.id);
console.log('게시물 수:', posts.length);
// 병렬 실행이 필요하면 Promise.all 사용
const [comments, likes] = await Promise.all([
fetchComments(posts[0].id),
fetchLikes(posts[0].id)
]);
return { user, posts, comments, likes };
} catch (error) {
console.error('처리 중 오류:', error.message);
throw error; // 에러 재전파
}
}
// async 함수 호출 (Promise 반환)
processUser(1).then(data => console.log('완료:', data));
"콜백은 함수를 인자로 전달해서 비동기 결과를 받지만, 중첩되면 콜백 지옥이 발생합니다. Promise는 then 체이닝으로 평탄하게 작성할 수 있고, catch로 에러를 한 곳에서 처리할 수 있습니다. 또한 Promise.all로 병렬 처리를 쉽게 구현할 수 있어 콜백보다 유연합니다."
"이 async 함수에 try-catch가 없네요. await에서 reject되면 에러가 전파되어 unhandled rejection 경고가 발생합니다. try-catch로 감싸거나, 호출하는 쪽에서 .catch()를 추가해주세요. Node.js에서는 process.on('unhandledRejection')으로 전역 핸들러도 설정해두는 게 좋습니다."
"이 세 API 호출은 서로 의존성이 없으니까 순차 await 대신 Promise.all로 병렬 실행하면 응답 시간을 3초에서 1초로 줄일 수 있어요. 다만 하나라도 실패하면 전체가 reject되니까, 부분 실패를 허용하려면 Promise.allSettled를 사용해야 합니다."
Promise에서 reject가 발생했는데 catch()나 try-catch로 처리하지 않으면 경고가 발생합니다. Node.js 15+에서는 프로세스가 종료될 수 있습니다. 모든 Promise 체인은 반드시 .catch()로 끝내거나, async 함수 내에서 try-catch를 사용하세요.
독립적인 비동기 작업을 순차적으로 await하면 불필요하게 느려집니다. `await a(); await b();` 대신 `await Promise.all([a(), b()]);`로 병렬 실행하세요. 단, 순서가 중요하거나 앞 결과가 뒤 호출에 필요한 경우는 순차 실행이 맞습니다.
async 함수는 항상 Promise를 반환합니다. `return 값`은 `Promise.resolve(값)`과 동일합니다. forEach 콜백에서 async를 쓰면 반환된 Promise가 무시되므로, for...of 루프나 Promise.all(arr.map(...))을 사용하세요.
간단한 비동기 흐름은 async/await이 가독성이 좋습니다. 병렬 처리에는 Promise.all, 부분 실패 허용에는 Promise.allSettled, 가장 빠른 결과만 필요하면 Promise.race를 사용하세요. 이벤트 기반 코드는 여전히 콜백이 적합합니다.