
프로그래밍을 처음 배울 때 Promise는 보통 “미래에 받을 값을 담는 객체”라고 배운다.
const paymentPromise =
processPayment(orderId);
paymentPromise.then((payment) => {
console.log(payment);
});
결제 작업이 완료되면 payment를 사용할 수 있다는 기본 개념을 이해하기에는 충분한 설명이다.
하지만 실제 결제 서비스를 개발하면 미래의 값을 받는 것보다 더 많은 판단이 필요하다.
Promise는 단순히 나중에 생길 값을 보관하는 상자가 아니다.
Promise는 비동기 작업의 완료와 실패를 표현하고, 그 결과에 다음 작업을 연결하는 계약이다.
다음 코드를 살펴보자.
const paymentPromise = new Promise(
(resolve) => {
console.log("결제 요청 시작");
resolve({
paymentId: "pay_42",
status: "approved",
});
}
);
console.log("Promise 생성 완료");
Promise 생성자에 전달한 함수는 Promise를 만드는 순간 바로 실행된다.
출력 순서는 다음과 같다.
결제 요청 시작
Promise 생성 완료
Promise 안의 결과를 사용하는 코드는 나중에 실행될 수 있지만, Promise가 표현하는 작업 자체가 반드시 나중에 시작되는 것은 아니다.
실제 서비스에서는 이 차이가 중요하다.
const paymentPromise =
requestPayment(orderId);
// 다른 작업 수행
const payment =
await paymentPromise;
이 코드에서 결제 요청은 await를 만났을 때 시작되는 것이 아니다. requestPayment(orderId)를 호출한 시점에 이미 시작되었을 수 있다.
작업을 실제로 나중에 시작하려면 Promise 자체가 아니라 Promise를 생성하는 함수를 전달해야 한다.
const startPayment = () =>
requestPayment(orderId);
// 필요한 시점에 작업 시작
const payment =
await startPayment();
startPayment는 아직 결제를 실행하지 않은 작업 계획이다. 함수를 호출하는 순간 결제 요청이 시작된다.
따라서 다음 두 값은 의미가 다르다.
const runningPayment =
requestPayment(orderId);
const paymentTask = () =>
requestPayment(orderId);
runningPayment는 이미 시작된 작업의 완료를 표현한다. paymentTask는 호출할 때 새 작업을 시작할 수 있는 함수다.
Promise를 이해할 때는 “값이 언제 도착하는가”뿐 아니라 “작업이 언제 시작되는가”도 구분해야 한다.
Promise는 일반적으로 다음 세 가지 상태를 가진다고 배운다.
pending → fulfilled
↘ rejected
이 상태를 외우는 것만으로는 결제 서비스의 흐름을 설계하기 어렵다.
각 상태를 서비스의 의미로 바꾸어 생각해야 한다.
pending: 결제 결과가 아직 확정되지 않았다.fulfilled: 비동기 작업이 정해진 방식으로 완료되었다.rejected: 비동기 작업을 정상적으로 완료하지 못했다.여기서 fulfilled가 반드시 비즈니스 성공을 뜻하지는 않는다.
type PaymentResult =
| {
status: "approved";
paymentId: string;
}
| {
status: "declined";
reason: string;
};
async function processPayment(
input: PaymentInput
): Promise<PaymentResult> {
return {
status: "declined",
reason: "insufficient_funds",
};
}
이 Promise는 정상적으로 이행된다. 다만 결과가 결제 승인 거절을 나타낸다.
카드 잔액 부족처럼 예상 가능한 비즈니스 결과는 정상적인 응답으로 표현할 수 있다. 반면 결제 제공자와 통신하지 못했거나 응답 형식을 해석할 수 없다면 Promise를 거부할 수 있다.
async function processPayment(
input: PaymentInput
): Promise<PaymentResult> {
const response =
await paymentProvider.charge(input);
if (!response) {
throw new Error(
"Payment provider returned no response"
);
}
return mapPaymentResult(response);
}
Promise의 성공과 결제의 승인은 같은 개념이 아니다.
이 구분이 없으면 예상 가능한 결제 거절까지 서버 오류처럼 처리하거나, 실제 시스템 오류를 정상적인 결제 결과처럼 숨길 수 있다.
then은 결과를 읽는 기능이 아니라 새로운 작업을 연결한다다음 코드는 결제 결과를 출력한다.
processPayment(input).then((payment) => {
console.log(payment);
});
겉으로 보면 then이 Promise 안의 값을 꺼내는 기능처럼 보인다.
그러나 then은 기존 Promise를 변경하지 않는다. 항상 새로운 Promise를 반환한다.
const paymentPromise =
processPayment(input);
const receiptPromise =
paymentPromise.then((payment) => {
return createReceipt(payment);
});
두 Promise는 서로 다른 완료를 표현한다.
paymentPromise는 결제 처리의 완료를 표현한다.receiptPromise는 결제 이후 영수증 생성까지의 완료를 표현한다.이 차이는 다음 작업을 어디에 연결할지 결정할 때 중요하다.
const resultPromise =
processPayment(input)
.then((payment) => {
return createReceipt(payment);
})
.then((receipt) => {
return sendReceiptEmail(receipt);
});
각 then이 반환한 값은 다음 단계의 입력이 된다.
이를 서비스 흐름으로 표현하면 다음과 같다.
결제 처리
↓ Payment
영수증 생성
↓ Receipt
영수증 이메일 전송
↓ EmailResult
전체 흐름 완료
Promise 체인은 단순히 코드를 순서대로 적는 문법이 아니다. 한 작업의 완료가 다음 작업의 입력이 되는 의존 관계를 표현한다.
다음 구현에는 눈에 잘 보이지 않는 문제가 있다.
processPayment(input)
.then((payment) => {
createReceipt(payment);
})
.then(() => {
showPaymentComplete();
});
createReceipt가 Promise를 반환하더라도 첫 번째 then에서 그 Promise를 반환하지 않았다.
따라서 다음 then은 영수증 생성을 기다리지 않는다.
processPayment(input)
.then((payment) => {
return createReceipt(payment);
})
.then(() => {
showPaymentComplete();
});
이제 첫 번째 then이 영수증 생성 Promise를 반환한다. 다음 단계는 영수증 생성이 끝날 때까지 기다린다.
화살표 함수에서는 더 간결하게 작성할 수 있다.
processPayment(input)
.then((payment) =>
createReceipt(payment)
)
.then(() => {
showPaymentComplete();
});
이 차이는 실제 서비스에서 큰 결과를 만든다.
Promise를 반환하지 않으면 다음과 같은 일이 발생할 수 있다.
비동기 작업을 연결한다는 것은 단지 함수 안에서 다른 함수를 호출하는 것이 아니다. 그 작업의 Promise를 반환해 완료와 실패의 책임을 다음 단계에 전달하는 것이다.
then 안에서 반드시 Promise만 반환해야 하는 것은 아니다.
const amountPromise =
loadOrder(orderId)
.then((order) => {
return order.totalAmount;
});
order.totalAmount는 일반 숫자지만 amountPromise는 그 숫자로 이행되는 Promise가 된다.
다음처럼 Promise를 반환할 수도 있다.
const paymentPromise =
loadOrder(orderId)
.then((order) => {
return processPayment({
orderId: order.id,
amount: order.totalAmount,
});
});
이 경우 바깥 Promise는 내부 결제 Promise가 끝날 때까지 기다리고 같은 결과를 이어받는다.
예외를 던지면 새로운 Promise는 거부된다.
const paymentPromise =
loadOrder(orderId)
.then((order) => {
if (order.status !== "pending") {
throw new Error(
"Order is not payable"
);
}
return processPayment({
orderId: order.id,
amount: order.totalAmount,
});
});
따라서 then의 반환 방식은 다음 단계의 의미를 결정한다.
| 콜백의 결과 | 새 Promise의 상태 |
|---|---|
| 일반 값 반환 | 해당 값으로 이행 |
| Promise 반환 | 반환한 Promise의 결과를 기다림 |
| 예외 발생 | 예외를 이유로 거부 |
| 아무것도 반환하지 않음 | undefined로 이행 |
아무것도 반환하지 않는 것도 하나의 결과다. JavaScript는 이를 undefined로 처리한다.
그래서 반환 누락은 단순한 문법 실수가 아니라 작업 사이의 연결을 끊는 설계 문제가 될 수 있다.
catch까지 작업 흐름을 따라간다다음 결제 흐름을 생각해 보자.
processPayment(input)
.then((payment) =>
createReceipt(payment)
)
.then((receipt) =>
sendReceiptEmail(receipt)
)
.catch((error) => {
recordPaymentFlowError(error);
});
다음 상황은 모두 마지막 catch에 도착할 수 있다.
processPayment가 거부된다.createReceipt가 거부된다.sendReceiptEmail이 거부된다.then 안에서 동기적인 예외가 발생한다.이 전파 방식은 중앙에서 오류를 처리할 수 있게 하지만, 모든 오류를 같은 의미로 만들어 주지는 않는다.
processPayment(input)
.then((payment) =>
createReceipt(payment)
)
.catch(() => {
showMessage(
"결제를 완료하지 못했다."
);
});
영수증 생성만 실패했는데 사용자에게 결제 자체가 실패했다고 알려주면 실제 상태와 다른 메시지가 된다.
결제는 이미 승인되었을 수 있다. 이때 다시 결제를 시도하도록 안내하면 중복 결제가 발생할 수 있다.
작업별 실패 의미를 구분하려면 경계를 명시해야 한다.
const payment =
await processPayment(input);
try {
await createReceipt(payment);
} catch (error) {
await recordReceiptFailure({
paymentId: payment.paymentId,
error,
});
}
return payment;
결제 승인은 핵심 결과로 유지한다. 영수증 생성 실패는 별도로 기록하고 재처리할 수 있다.
하나의 catch가 여러 기술적 오류를 받을 수 있다는 사실과, 모든 오류를 같은 사용자 메시지로 처리해도 된다는 판단은 다르다.
catch에서 값을 반환하면 흐름은 복구된 것으로 처리된다다음 코드를 살펴보자.
const payment =
await processPayment(input)
.catch((error) => {
recordError(error);
return {
status: "approved",
paymentId: "unknown",
};
});
catch가 값을 반환했기 때문에 바깥 Promise는 이 값으로 이행된다.
오류를 기록했더라도 Promise 관점에서는 복구가 완료된 것이다.
이 패턴을 잘못 사용하면 실패한 결제를 성공으로 바꿀 수 있다.
오류를 기록한 뒤 상위 단계에서도 실패를 처리해야 한다면 다시 예외를 던져야 한다.
const payment =
await processPayment(input)
.catch((error) => {
recordError(error);
throw error;
});
반대로 명확한 대체값이 있다면 catch에서 복구할 수 있다.
const exchangeRate =
await loadExchangeRate()
.catch((error) => {
recordExchangeRateFailure(error);
return configuredFallbackRate;
});
이 경우에는 미리 정의된 환율을 사용하는 것이 허용된다는 비즈니스 정책이 있어야 한다.
catch는 단순한 오류 출력 위치가 아니다. 실패를 계속 전달할지, 다른 결과로 복구할지를 결정하는 경계다.
finally는 성공과 실패를 바꾸지 않는 정리 작업에 사용한다결제 버튼의 로딩 상태를 정리한다고 생각해 보자.
setPaymentStatus("submitting");
try {
await processPayment(input);
setPaymentStatus("success");
} catch {
setPaymentStatus("error");
} finally {
releasePaymentLock();
}
finally는 성공과 실패 모두에서 실행된다.
잠금 해제, 타이머 정리, 임시 자원 반환처럼 결과와 무관하게 필요한 작업에 적합하다.
그러나 finally에서 값을 반환하거나 예외를 던지면 기존 결과를 바꿀 수 있다.
async function submitPayment() {
try {
return await processPayment(input);
} finally {
return {
status: "unknown",
};
}
}
이 코드는 실제 결제 결과 대신 finally가 반환한 값을 사용한다.
다음 코드에서는 정리 작업의 오류가 원래 결제 오류를 가릴 수도 있다.
try {
await processPayment(input);
} finally {
await disconnectPaymentSession();
}
processPayment와 disconnectPaymentSession이 모두 실패하면 호출자는 정리 작업의 오류만 받을 수 있다.
따라서 finally에는 일반적으로 결과를 결정하지 않는 정리 작업을 둔다. 정리 실패 자체도 중요하다면 원래 오류와 함께 기록할 방법을 별도로 설계해야 한다.
결제 완료 후 다음 작업을 실행한다고 생각해 보자.
const tasks = [
sendReceiptEmail(payment),
updateRewardPoints(payment),
recordAnalyticsEvent(payment),
];
이 작업들을 어떻게 묶을지는 “동시에 실행할 수 있는가”만으로 결정되지 않는다. 하나가 실패했을 때 나머지 결과를 어떻게 취급할지도 정해야 한다.
await Promise.all([
reserveOrderItems(order),
createPaymentRecord(payment),
]);
Promise.all은 모든 작업이 이행되어야 성공한다. 하나가 거부되면 반환된 Promise도 거부된다.
다만 다른 작업이 자동으로 취소되는 것은 아니다. 하나가 실패한 뒤에도 이미 시작된 작업은 계속 실행될 수 있다.
재고 확보와 결제 기록처럼 함께 성공해야 하는 작업이라면 Promise.all만으로 원자성을 보장할 수 없다. 데이터베이스 트랜잭션이나 보상 작업 같은 별도의 일관성 설계가 필요하다.
const results =
await Promise.allSettled([
sendReceiptEmail(payment),
updateRewardPoints(payment),
recordAnalyticsEvent(payment),
]);
Promise.allSettled는 각 작업의 성공과 실패를 모두 반환한다.
이메일 실패를 기록하면서 포인트 적립과 분석 이벤트의 결과도 확인해야 할 때 사용할 수 있다.
for (const result of results) {
if (result.status === "rejected") {
recordFollowUpFailure(
result.reason
);
}
}
한 작업의 실패가 다른 작업의 결과를 가리지 않는다.
const verification =
await Promise.any([
verifyWithPrimaryProvider(input),
verifyWithBackupProvider(input),
]);
Promise.any는 가장 먼저 이행된 결과를 사용한다. 모든 작업이 거부될 때만 전체 Promise가 거부된다.
이 방식은 여러 대체 수단 중 하나의 성공이면 충분한 경우에 적합하다. 그러나 같은 결제를 여러 제공자에게 동시에 요청하는 방식으로 사용하면 중복 청구 위험이 생긴다.
Promise 결합 방식은 문법 선택이 아니라 서비스의 실패 정책을 표현한다.
Promise.race는 작업을 취소하지 않는다시간 제한을 다음처럼 구현할 수 있다.
const result =
await Promise.race([
processPayment(input),
rejectAfter(5000),
]);
5초 안에 결제 결과가 도착하지 않으면 시간 제한 Promise가 먼저 거부될 수 있다.
하지만 Promise.race에서 졌다고 해서 결제 요청이 취소되는 것은 아니다.
결제 작업은 서버나 외부 결제 제공자에서 계속 처리될 수 있다.
따라서 다음과 같은 안내는 위험하다.
catch {
showMessage(
"결제에 실패했다. 다시 시도해 달라."
);
}
클라이언트가 결과를 받지 못했을 뿐 결제가 승인되었을 수 있다. 사용자가 즉시 다시 시도하면 중복 결제가 발생할 수 있다.
결제처럼 상태를 변경하는 작업에는 작업 ID와 멱등성 키가 필요하다.
const paymentAttemptId =
crypto.randomUUID();
await fetch("/api/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key":
paymentAttemptId,
},
body: JSON.stringify(input),
});
응답을 확인하지 못했다면 같은 키로 재시도하거나 결제 상태 조회 API에서 실제 결과를 확인할 수 있어야 한다.
Promise의 거부는 클라이언트가 작업의 결과를 얻지 못했다는 뜻일 수 있다. 서버에서 비즈니스 작업이 실행되지 않았다는 증거는 아니다.
다음 함수는 PaymentResponse를 반환하는 Promise로 선언되어 있다.
type PaymentResponse = {
paymentId: string;
status: "approved" | "declined";
amount: number;
};
async function requestPayment(
input: PaymentInput
): Promise<PaymentResponse> {
const response =
await fetch("/api/payments", {
method: "POST",
body: JSON.stringify(input),
});
return response.json();
}
TypeScript의 반환 타입은 실제 서버 응답을 검증하지 않는다.
서버가 paymentId를 누락하거나 amount를 문자열로 반환해도 response.json()은 런타임에서 그대로 값을 전달한다.
외부에서 도착한 값은 검증 전까지 신뢰할 수 없다.
const paymentResponseSchema =
z.object({
paymentId: z.string().min(1),
status: z.enum([
"approved",
"declined",
]),
amount: z.number().nonnegative(),
});
async function requestPayment(
input: PaymentInput
): Promise<PaymentResponse> {
const response =
await fetch("/api/payments", {
method: "POST",
body: JSON.stringify(input),
});
const data = await response.json();
return paymentResponseSchema.parse(
data
);
}
이제 네트워크 요청의 완료와 응답 데이터의 검증이 서로 다른 단계로 표현된다.
Promise가 이행되었다는 사실은 그 안의 데이터가 올바르다는 뜻이 아니다. Promise는 작업의 완료를 표현하지만, 결과의 비즈니스 유효성은 별도로 검증해야 한다.
프론트엔드에서 다음 Promise가 이행되었다고 생각해 보자.
const payment =
await requestPayment(input);
setPaymentStatus(payment.status);
이 결과는 서버가 반환한 특정 시점의 복사본이다.
이후 환불, 승인 취소, 외부 결제 제공자의 지연된 상태 변경이 발생할 수 있다.
type PaymentStatus =
| "pending"
| "approved"
| "declined"
| "cancelled"
| "refunded";
화면이 가지고 있는 approved 상태를 영구적인 사실로 간주해서는 안 된다.
결제 상태의 Source of Truth는 서버와 데이터베이스가 관리하는 최신 결제 기록이다. 외부 결제 제공자가 최종 상태를 관리한다면 웹훅과 상태 동기화도 필요하다.
const latestPayment =
await getPaymentStatus(paymentId);
이 Promise는 최신 상태 조회 작업의 완료를 표현할 뿐이다. Promise 자체가 최신성이나 진실성을 보장하는 것은 아니다.
Promise가 무엇을 완료했는지와 그 결과가 어떤 데이터 출처를 기준으로 만들어졌는지를 함께 확인해야 한다.
다음 구현은 결제와 모든 후속 작업을 하나의 체인으로 묶는다.
return processPayment(input)
.then(createReceipt)
.then(sendReceiptEmail)
.then(updateRewardPoints)
.then(recordAnalyticsEvent);
코드는 간결하지만 마지막 Promise가 무엇을 의미하는지 불분명하다.
핵심 결과와 후속 작업의 책임을 분리할 수 있다.
async function completePayment(
input: PaymentInput
) {
const payment =
await processPayment(input);
await paymentEventQueue.publish({
type: "payment.approved",
paymentId: payment.paymentId,
});
return payment;
}
이 함수가 반환하는 Promise는 결제 처리와 후속 이벤트 등록까지의 완료를 표현한다.
별도의 작업자가 후속 책임을 처리한다.
async function handlePaymentApproved(
event: PaymentApprovedEvent
) {
const results =
await Promise.allSettled([
createAndSendReceipt(
event.paymentId
),
updateRewardPoints(
event.paymentId
),
recordAnalyticsEvent(
event.paymentId
),
]);
await saveFollowUpResults(
event.paymentId,
results
);
}
여기서는 후속 작업의 개별 결과를 저장해 실패한 작업을 다시 처리할 수 있다.
다만 이벤트 등록 자체가 실패하면 결제는 승인되었지만 후속 처리가 사라질 수 있다. 결제 기록과 이벤트를 함께 보장해야 한다면 트랜잭셔널 아웃박스 같은 추가 설계가 필요하다.
Promise 체인을 짧게 만드는 것보다 각 Promise가 어디까지의 완료를 보장하는지 명확하게 만드는 것이 중요하다.
결제 화면에서는 Promise 자체를 표시할 수 없다.
사용자가 이해할 수 있는 상태로 변환해야 한다.
type PaymentViewState =
| { status: "idle" }
| { status: "submitting" }
| {
status: "approved";
paymentId: string;
}
| {
status: "declined";
message: string;
}
| {
status: "unknown";
paymentAttemptId: string;
}
| {
status: "error";
message: string;
};
각 상태는 서로 다른 다음 행동을 제공한다.
idle: 사용자가 결제를 시작할 수 있다.submitting: 중복 제출을 줄이고 처리 중임을 알린다.approved: 승인 결과와 영수증 정보를 보여준다.declined: 결제 수단을 변경하도록 안내한다.unknown: 결제 상태를 다시 조회하도록 안내한다.error: 복구 가능한 방법이나 문의 정보를 제공한다.특히 unknown은 결제 서비스에서 중요하다.
네트워크가 끊겨 Promise가 거부되었더라도 서버 결과가 확정되지 않았을 수 있기 때문이다. 이 상태를 곧바로 declined나 error로 바꾸면 기술적 실패와 비즈니스 결과를 혼동하게 된다.
Promise의 상태는 기술적 완료를 표현한다. 화면 상태는 그 결과를 사용자의 언어와 다음 행동으로 번역해야 한다.
하나의 결제는 프론트엔드 함수 안에서 끝나지 않는다.
flowchart LR
A[결제 버튼 클릭] --> B[결제 시도 ID 생성]
B --> C[입력 검증]
C --> D[결제 요청 Promise]
D --> E[서버의 주문 상태 확인]
E --> F[결제 제공자 요청]
F --> G{처리 결과}
G -->|승인| H[결제 기록 저장]
G -->|거절| I[거절 결과 반환]
G -->|결과 불확실| J[상태 조회]
H --> K[후속 이벤트 등록]
K --> L[영수증·포인트·분석]
H --> M[완료 화면]
D -. 네트워크 단절 .-> J
각 단계의 Promise는 서로 다른 완료를 나타낸다.
모든 Promise가 이행되었다고 해서 같은 의미의 성공이 발생한 것은 아니다.
각 Promise가 무엇의 완료를 보장하며, 실패했을 때 누가 복구할 책임을 가지는지 정의해야 한다.
then에서 다음 Promise를 반환하고 있는가?catch가 실패를 숨기고 성공 값으로 바꾸지는 않는가?Promise.race 이후에도 나머지 작업이 계속 실행됨을 고려했는가?const paymentPromise =
requestPayment(input);
requestPayment를 호출하는 순간 작업이 시작될 수 있다.
실행을 미루려면 Promise가 아니라 함수를 보관해야 한다.
const startPayment = () =>
requestPayment(input);
then 안에서 Promise를 반환하지 않는다processPayment(input)
.then((payment) => {
createReceipt(payment);
})
.then(showPaymentComplete);
다음 단계는 영수증 생성을 기다리지 않는다.
processPayment(input)
.then((payment) => {
return createReceipt(payment);
})
.then(showPaymentComplete);
반환을 통해 영수증 생성의 완료와 실패가 다음 단계에 연결된다.
try {
await completePaymentFlow(input);
} catch {
showMessage("결제에 실패했다.");
}
영수증 이메일이나 분석 기록만 실패했을 수도 있다.
결제 승인 결과와 후속 작업 결과를 구분해야 한다.
catch에서 임의의 값을 반환해 실패를 숨긴다const payment =
await processPayment(input)
.catch(() => ({
status: "approved",
paymentId: "unknown",
}));
catch가 반환한 값은 정상적인 이행 결과가 된다.
명시적인 복구 정책이 없다면 오류를 다시 전달해야 한다.
Promise.all이 작업을 하나의 트랜잭션으로 만든다고 생각한다await Promise.all([
reserveOrderItems(order),
chargeCustomer(order),
]);
하나가 실패해도 다른 작업은 이미 완료되었을 수 있다.
함께 성공하거나 함께 실패해야 한다면 데이터베이스 트랜잭션, 멱등성 또는 보상 작업이 필요하다.
Promise.race가 느린 작업을 취소한다고 생각한다await Promise.race([
processPayment(input),
rejectAfter(5000),
]);
시간 제한이 먼저 끝나도 결제 작업은 계속 실행될 수 있다.
결과 조회와 중복 방지 설계 없이 바로 다시 결제해서는 안 된다.
const payment =
(await response.json())
as PaymentResponse;
타입 단언은 외부 데이터를 검증하지 않는다.
스키마 검증을 통과한 값만 결제 결과로 사용해야 한다.
Promise를 처음 배울 때는 다음 설명만으로 기본 동작을 이해할 수 있다.
const payment =
await processPayment(input);
결제가 끝난 뒤 결과를 받아 다음 코드를 실행한다는 설명은 입문 단계에서 충분하다.
하지만 실제 서비스에서는 Promise마다 서로 다른 책임과 의미가 있다.
catch는 실패를 복구하는가, 숨기는가?Promise는 미래의 값을 담는 객체가 아니다.
비동기 작업의 완료와 실패를 표현하고, 그 결과에 다음 작업과 복구 책임을 연결하는 계약이다.