서버에 데이터를 요청했는데 응답이 언제 올지 모릅니다. 그 사이에 화면은 멈춰 있어야 할까요? JavaScript는 이 문제를 Promise로 해결합니다.
Promise는 "지금 당장은 비어 있지만, 나중에 값이 채워질 상자"입니다. 상자를 먼저 건네받고, 값이 채워지면 그때 꺼내 쓰는 방식입니다. 비동기 작업의 결과를 동기 코드처럼 순서 있게 다룰 수 있게 해줍니다.
Promise는 항상 세 가지 상태 중 하나에 있습니다.
new Promise(...)
│
▼
[ pending ] ── 작업 진행 중
│
┌────┴────┐
▼ ▼
[fulfilled] [rejected]
성공, 값 있음 실패, 이유 있음
한 번 fulfilled 또는 rejected 상태가 되면 다시 변하지 않습니다. 상태는 단방향으로만 전이됩니다.
new Promise는 executor 함수를 받습니다. executor 안에서 비동기 작업을 수행하고, 성공하면 resolve, 실패하면 reject를 호출합니다.
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice" });
} else {
reject(new Error("유효하지 않은 ID입니다."));
}
}, 1000);
});
}
then은 성공 결과를, catch는 실패 이유를 받습니다. finally는 성공·실패에 관계없이 항상 실행됩니다.
fetchUser(1)
.then((user) => {
console.log("성공:", user.name); // "성공: Alice"
})
.catch((err) => {
console.error("실패:", err.message);
})
.finally(() => {
console.log("요청 완료"); // 항상 실행
});
then은 두 번째 인자로 실패 핸들러를 받을 수 있지만, 가독성을 위해 catch를 별도로 쓰는 것이 일반적입니다.
then이 반환하는 값은 다음 then의 입력으로 전달됩니다. 이를 통해 비동기 작업을 순서대로 연결할 수 있습니다.
fetchUser(1)
.then((user) => {
console.log("사용자:", user.name);
return user.id; // 다음 then으로 전달
})
.then((id) => {
return fetch(`/api/posts?userId=${id}`); // Promise 반환 가능
})
.then((response) => response.json())
.then((posts) => {
console.log("게시글 수:", posts.length);
})
.catch((err) => {
console.error("오류:", err.message);
});
then에서 Promise를 반환하면 체인은 그 Promise가 완료될 때까지 기다립니다.
체인 중간에서 에러가 발생하면 이후 then은 건너뛰고 가장 가까운 catch로 이동합니다.
fetchUser(1)
.then((user) => {
throw new Error("처리 중 문제 발생"); // 의도적 에러
})
.then((data) => {
console.log("이 코드는 실행되지 않습니다.");
})
.catch((err) => {
console.error("catch에서 처리:", err.message);
// "catch에서 처리: 처리 중 문제 발생"
});
catch 이후에도 then을 연결할 수 있습니다. catch가 값을 반환하면 체인이 다시 이어집니다.
여러 Promise를 동시에 실행하고, 모두 성공했을 때 결과 배열을 반환합니다. 하나라도 실패하면 즉시 전체가 실패합니다.
const p1 = fetchUser(1);
const p2 = fetchUser(2);
const p3 = fetchUser(3);
Promise.all([p1, p2, p3])
.then(([user1, user2, user3]) => {
console.log(user1.name, user2.name, user3.name);
})
.catch((err) => {
console.error("하나라도 실패:", err.message);
});
모든 Promise가 완료(성공 또는 실패)된 후, 각각의 결과를 수집합니다. 하나가 실패해도 나머지 결과를 잃지 않아야 할 때 사용합니다.
Promise.allSettled([fetchUser(1), fetchUser(-1), fetchUser(3)])
.then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("성공:", result.value.name);
} else {
console.log("실패:", result.reason.message);
}
});
});
가장 먼저 완료된 Promise의 결과(성공 또는 실패)를 반환합니다. 타임아웃 구현에 자주 활용됩니다.
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("시간 초과")), 3000)
);
Promise.race([fetchUser(1), timeout])
.then((user) => console.log("응답:", user.name))
.catch((err) => console.error(err.message));
가장 먼저 성공한 Promise의 결과를 반환합니다. 모두 실패할 경우 AggregateError를 던집니다. 여러 소스 중 하나라도 성공하면 충분할 때 사용합니다.
Promise.any([fetchUser(-1), fetchUser(2), fetchUser(3)])
.then((user) => {
console.log("첫 번째 성공:", user.name); // fetchUser(2)의 결과
})
.catch((err) => {
console.error("모두 실패:", err.errors);
});
Promise.race는 실패도 반환하지만, Promise.any는 성공만 반환합니다.
then 콜백은 동기 코드가 모두 끝난 직후, 이벤트 루프의 다음 태스크보다 먼저 실행됩니다. 이는 then 콜백이 마이크로태스크 큐에 등록되기 때문입니다.
console.log("1");
Promise.resolve().then(() => {
console.log("2"); // 마이크로태스크
});
setTimeout(() => {
console.log("3"); // 매크로태스크
}, 0);
console.log("4");
// 출력 순서: 1 → 4 → 2 → 3
동기 코드(1, 4)가 먼저 실행되고, 마이크로태스크(2)가 처리된 후 매크로태스크(3)가 실행됩니다.
마이크로태스크 큐(Microtask Queue): 현재 실행 중인 스크립트가 끝난 직후 실행되는 작업 대기열로, setTimeout 같은 매크로태스크보다 우선순위가 높습니다.
| 메서드 | 성공 조건 | 실패 조건 | 용도 |
|---|---|---|---|
Promise.all | 전부 성공 | 하나라도 실패 | 모든 결과가 필요할 때 |
Promise.allSettled | 전부 완료 | 없음 | 성공·실패 모두 수집할 때 |
Promise.race | 가장 먼저 완료 | 가장 먼저 실패 | 타임아웃, 속도 경쟁 |
Promise.any | 가장 먼저 성공 | 전부 실패 | 하나만 성공해도 충분할 때 |
Promise는 비동기 작업의 성공과 실패를 명확히 분리하고, 체이닝으로 복잡한 흐름을 읽기 쉽게 만듭니다. async/await도 내부적으로 Promise를 사용하므로, Promise를 이해하면 비동기 코드 전반을 다루는 기반이 됩니다.