
관성적으로 api 연동할 때마다 사용했던 Promise 개념을 처음부터 끝까지 한 번에 정리해보고자 합니다.
new 생성자를 통해 생성되며 생성과 동시에 실행 // new로 생성가능 -> 객체를 반환
const promiseCallback = new Promise(()=>{
// 인자로 콜백함수를 넣기 가능 -> 해당 콜백 함수 안에서 비동기 함수를 처리
});
Pending: 비동기 처리가 진행중이면 대기 상태Fulfilled : 작업 성공시, 이행 상태Rejected : 작업 실패시, 거부 상태 
→ 상태는 단 한 번만 바뀝니다.
WHY? 비동기 결과를 안정적으로 다루기 위해서입니다.
Promise는 지금은 없지만, 나중에 반드시 생기는 하나의 결과입니다.
결과는 성공 아니면 실패겠죠.
근데 만약에 결과가 여러번 바뀌면 어떻게 될까요?
지옥이 펼쳐집니다. 값을 예측하기 힘들겠죠. 그럼 디버깅 하기 힘들고 그럼 개발자는 눈물이 납니다.
SO, 첫 resolve, reject만 유효하고 이후 호출은 전부 무시합니다.
실제 코드로 실험해보면 다음과 같습니다.
const p = new Promise((resolve, reject) => {
resolve('성공1');
resolve('성공2');
reject('실패');
});
p.then(console.log).catch(console.error);
// "성공1"
resolve : fulfilled 상태로 확정시키는 함수reject : rejected 상태로 확정시키는 함수
then(): 이행되었을 때catch(): 거부되었을 때finally() : 이행되거나 거부되더라도 항상 실행→ 프로미스는 생성과 동시에 비동기 작업을 처리합니다.
⇒ then, catch일 때 비동기 작업을 처리하는 것이 아닙니다.
const myPromise = new Promise((resolve, reject)=>{
setTimeout(()=>{
const text = prompt('hello를 입력해봐~')
if(text === 'hello'){
resolve('success')
} else {
reject('hello 입력 안함 ㅠ')
}
}, 2000)
});
myPromise
.then((result)=> {
console.log('result:', result)
})
.catch((error)=>{
console.log('error:', error)
})
.finally(()=>{
console.log('final')
})
// hello를 입력하면 콘솔창에
// result: success
// hello를 입력하지 않으면 콘솔창에
// error: hello 입력 안함 ㅠ
promise.then(…) → 항상 새로운 Promise를 반환합니다.promise
.then(...)
.then(...)
.then(...) SO, 위와 같은 코드를 쓸 수 있게됩니다. ⇒ 같은 Promise를 이어 쓰는게 아니라 새로운 then마다 새로운 Promise가 생기는 구조입니다. 
then 안에서 return 하면 무슨 일이 일어날까?값을 반환합니다. → 다음 then에서 반환된 값을 사용할 수 있습니다
WHY? promise 객체를 새로 만드니까
myPromise
.then((result)=> {
console.log('result:', result)
return `결과는? ${result}`
})
.then((result)=> {
console.log('result:', result)
})
// hello를 입력하면 콘솔창에 아래와 같이 나옴
// result: success
// result: 결과는? success
// final
then은 여러개 붙여도 순서가 보장될까?예 보장됩니다. then은 이전 Promise가 fulfilled 상태가 되어야 다음 Promise가 실행됩니다.
WHY? promise는 체이닝 구조로 비동기 흐름을 제어하니까요
.catch((error)=>{
console.log('error:', error)
})
.finally(()=>{ //결과에 상관없이 무조건 실행됨
console.log('final')
})
then은 왜 항상 비동기일까?Promise의 then은 “이미 끝난 Promise”라도 반드시 다음 턴에 실행됩니다.
Promise.resolve().then(() => {
console.log('then');
});
console.log('sync');
// 결과
// sync
// then
이는 실행 순서를 예측 가능하게 하고, 동기/비동기 혼합으로 인한 문제를 방지하기 위함이며, Microtask Queue 규칙에 따라 처리되기 때문입니다.
Microtask Queue란?
Call stack → Microtask queue→ Macrotask queue입니다.console.log(1); // Call stack
Promise.resolve().then(() => {
console.log(2); // Microtask queue
});
setTimeout(() => {
console.log(3);// Macrotask queue
});
console.log(4); // Call stack
//결과
1
4
2
3SO, then은 Promise의 상태와 상관없이 항상 Microtask Queue에 등록되어 현재 실행 중인 동기 코드가 모두 끝난 뒤 실행되도록 설계되었습니다.비동기 작업을 처리하는 함수에 성공 콜백과 실패 콜백을 각각 넘겨서 완료 상태에 따른 처리를 했습니다.
doSomething((err, result) => {
if (err) { /* 실패 */ }
else { /* 성공 */ }
});
→ BUT, 여러 비동기 작업이 순차적으로 의존해야 할 경우!
콜백 함수안에 콜백 함수가 중첩되는 callback hell이 펼쳐집니다
⇒ SO, 코드 가독성 + 유지보수성 저하
// 콜백 지옥
doA(() => {
doB(() => {
doC(() => {});
});
});
// 체이닝
doA()
.then(doB)
.then(doC);
→ 체이닝은 중첩이 아니라 흐름을 제어하는 것입니다.| 메서드 | 언제 끝남 | 실패 처리 | 핵심 용도 |
|---|---|---|---|
| Promise.all | 전부 끝나야 | 하나라도 실패 → 즉시 reject | 다 성공해야 의미 있을 때 |
| Promise.allSettled | 전부 끝나야 | 실패해도 reject ❌ | 성공/실패 전부 보고 싶을 때 |
| Promise.race | 제일 먼저 끝난 하나 | 그 결과 그대로 | 타임아웃 / 경쟁 |
| Promise.any | 제일 먼저 성공 | 전부 실패해야 reject | 대안 서버, fallback |
Promise.all모두 성공해야 성공! 하나라도 실패하면 즉시 reject됩니다
await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
언제 사용?
→ 하나라도 실패하면 나머지 결과는 무시됩니다.
BUT 요청 자체가 취소되지는 않습니다.
WHY? Promise에는 취소 개념이 없습니다.
Promise가 할 수 있는 건 상태(pending / fulfilled / rejected)를 관찰하고 결과를 전달하는 것입니다.
즉, Promise는 실행 중인 비동기 작업을 중단하지 못합니다.
fetch, setTimeout, ajax를 강제 종료할 힘이 없어요.
HOWEVER! 취소가 필요할 경우 AbortController 같은 별도 메커니즘을 사용해야 합니다.
Promise.allSettled
성공/실패 여부 상관없이 모두 완료될 때까지 대기
const results = await Promise.allSettled([
uploadImage(),
uploadVideo(),
]);
// 결과
[
{ status: 'fulfilled', value: ... },
{ status: 'rejected', reason: ... }
]
언제 사용?
Promise.race
가장 먼저 끝난 것 하나만 반환
Promise.race([
fetch('/api'),
timeout(3000),
]);
언제 사용?
성공이든 실패든 먼저 끝난 것 기준
HOW? 상태가 제일 먼저 확정된 Promise를 그대로 따라갑니다.
const fast = new Promise(resolve =>
setTimeout(() => resolve('fast'), 100)
);
const slow = new Promise(resolve =>
setTimeout(() => resolve('slow'), 1000)
);
Promise.race([fast, slow])
.then(result => console.log(result));
// 결과
// fast
BUT 나머지 코드 실행을 멈추는게 아니라 결과만 무시할 뿐 코드 실행은 계속됩니다.
Promise.any
가장 먼저 성공한 Promise 반환
Promise.any([
fetchFromA(),
fetchFromB(),
]);
언제 사용?
모두 실패하면 AggregateError 발생
then / catch는 하나의 Promise 흐름을 이어가는 인스턴스 메서드입니다.
→ Promise의 결과를 받아 다음 비동기 작업으로 전달함으로써, 비동기 코드를 동기 코드처럼 읽히게 만들어 흐름을 제어합니다.
반면 Promise.all / Promise.allSettled는 여러 Promise를 하나의 Promise로 조합하는 정적 메서드입니다.
→ 각 Promise는 서로 결과를 공유하지 않고 병렬로 실행되며, 모든 작업의 완료 상태를 기준으로 하나의 결과를 반환합니다.
async 키워드로 정의한 함수 내에서 호출되는 promise 앞에 await 키워드를 쓰면 해당 promise가 완료될 때까지 코드의 실행을 일시중단할 수 있습니다.async function fetchData(){
try{
const response = await fetch('https:주소')
const data = await response.json()
console.log(data)
}catch(error){
console.log('Fetch Error:', error)
}
} → fetch동작이 완료될 때까지 아래 부분을 실행하지 않아요await 에러 핸들링은 반드시 try-catch 블록에서 해야합니다.
try {
await asyncFn();
} catch (e) {
// 에러는 이 안에서만 잡힘
}
BUT await는 promise가 완료될 때까지 함수의 실행을 중단하기 때문에, 실행흐름을 잘 고려하여 적재적소에 써야합니다.
ex) 여러 비동기 작업이 순차적으로 진행될 필요가 없는 경우
async function fetchExp(){
try{
const result1 = await fetch('https://api/result1')
const result2 = await fetch('https://api/result2')
console.log(await result1.json(), await result2.json())
}catch(error){
console.log('Fetch Error:', error)
}
}
실행흐름
1. fetch('result1') 요청 보냄
2. result1 응답이 올 때까지 대기
3. fetch('result2') 요청 보냄
4. result2 응답 대기
5. 둘 다 끝나면 출력
⇒ 즉, 두 fetch가 "직렬(순차)"로 실행됩니다.
BUT result1의 값이 다음 fetch 동작에 사용되지 않습니다. d
→ result1 결과가 result2 요청에 전혀 영향 없기 때문에 순차적으로 실행될 필요가 없어요
이럴 때는 async-await 보다는 promise.all을 사용하는 것이 더 바람직합니다.
promise.all 버전
async function fetchExp() {
try {
const [result1, result2] = await Promise.all([
fetch('https://api/result1'),
fetch('https://api/result2')
])
console.log(
await result1.json(),
await result2.json()
)
} catch (error) {
console.log('Fetch Error:', error)
}
}
fetch1 즉시 요청
fetch2 즉시 요청
둘 다 동시에 날아감
둘 다 끝날 때까지 기다림
결과 사용
⇒ 즉, 두 fetch가 "병렬"로 실행됩니다.
미래에 완료될 하나의 비동기 결과를 값처럼 다루기 위한 객체
pending → fulfilled / rejected로 한 번만 확정됨
Promise를 fulfilled 또는 rejected 상태로 확정하는 함수
이전 Promise의 결과를 받아 다음 Promise로 이어주는 흐름 제어 메서드
체이닝 중 발생한 에러를 처리하는 메서드
성공/실패 여부와 관계없이 항상 실행되는 후처리 메서드
then이 새로운 Promise를 반환하면서 비동기 흐름을 연결하는 구조
실행 순서의 일관성과 안정성을 보장하기 위함
Promise의 then / catch 콜백을 실행하기 위한 우선순위가 높은 작업 큐
Promise 체이닝을 동기 코드처럼 작성하게 해주는 문법
모든 Promise가 성공해야 성공하는 병렬 조합 메서드
성공/실패와 무관하게 모든 Promise 결과를 수집하는 메서드
가장 먼저 상태가 확정된 Promise 하나의 결과를 따르는 메서드
가장 먼저 성공한 Promise 하나의 결과를 반환하는 메서드
"Promise를 쓴다" ≠ "Promise를 이해한다"
이제는 왜 이렇게 쓰는지 설명할 수 있으면 성공 ✨
https://www.youtube.com/watch?v=cDu9A5dl1J8&list=PLBh_4TgylO6CI4Ezq3OLRRzg2NAn3FLPB&index=2