Promise 3상태, new Promise 생성, .then/.catch/.finally 체이닝, 오류 전파, Promise.all/race/allSettled로 여러 비동기 작업 조합까지 완전 정복합니다.
🎯 학습 목표
이 강을 마치면 다음을 할 수 있습니다.
- Promise가 무엇인지, 왜 콜백 지옥을 해결하는지 이해합니다.
new Promise()로 Promise를 직접 만들 수 있습니다..then()과.catch(),.finally()로 비동기 흐름을 제어할 수 있습니다.Promise.all(),Promise.race()로 여러 Promise를 조합할 수 있습니다.
💡 Promise란 무엇인가
이 섹션에서는 Promise의 개념과 세 가지 상태를 살펴보겠습니다.
Promise는 비동기 작업의 미래 결과를 나타내는 객체입니다. 음식 주문에 비유할 수 있습니다. 주문하면 즉시 음식이 나오지 않고 언젠가 나올 것이라는 약속(Promise)을 받습니다. 음식이 나오면(fulfilled) 먹을 수 있고, 품절이면(rejected) 환불을 받습니다.
Promise는 세 가지 상태를 가집니다. Pending(대기): 비동기 작업이 아직 완료되지 않은 초기 상태. Fulfilled(이행): 작업이 성공적으로 완료된 상태, resolve가 호출됨. Rejected(거부): 작업이 실패한 상태, reject가 호출됨. 상태가 한 번 결정되면 다시 변경되지 않습니다.
// Promise 생성
const promise = new Promise((resolve, reject) => {
// executor 함수: Promise 생성 시 즉시 실행
const success = true;
if (success) {
resolve('성공 데이터'); // fulfilled 상태로 전환
} else {
reject(new Error('실패 이유')); // rejected 상태로 전환
}
});
// 실전: 타이머를 Promise로 감싸기
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 사용
delay(1000).then(() => console.log('1초 후 실행'));
// fetch API는 이미 Promise를 반환 (26강에서 자세히)
// const responsePromise = fetch('/api/data');
Promise 생성자는 executor 함수를 인수로 받습니다. executor는 resolve와 reject를 인수로 받아, 작업 완료 시 resolve(결과값)를, 실패 시 reject(오류)를 호출합니다. resolve나 reject를 한 번 호출하면 그 이후의 호출은 무시됩니다. 위 delay 함수처럼 기존 콜백 기반 API를 Promise로 감싸는 패턴을 Promise 래핑(Promisification)이라고 합니다.
🔗 then · catch · finally — 체이닝
이 섹션에서는 Promise의 결과를 처리하는 세 가지 메서드와 체이닝 패턴을 살펴보겠습니다.
Promise는 .then(), .catch(), .finally() 메서드를 제공합니다. 이 메서드들 자체도 새로운 Promise를 반환하므로 체이닝(chaining)이 가능합니다. 이것이 콜백 지옥과의 결정적 차이입니다. 중첩 없이 수평으로 이어집니다.
// 기본 체이닝 패턴
fetch('/api/users/1')
.then(response => response.json()) // Response → JSON 파싱 (Promise 반환)
.then(user => {
console.log(user.name);
return user.id; // 반환값이 다음 then의 인수로 전달
})
.then(userId => console.log('ID:', userId))
.catch(error => {
// 체인 중 어디서 오류가 발생해도 여기서 한 번에 처리
console.error('오류:', error.message);
})
.finally(() => {
// 성공/실패 관계없이 항상 실행
console.log('로딩 완료');
hideLoadingSpinner();
});
// delay 함수로 순차 실행 — 콜백 지옥 없이!
delay(1000)
.then(() => {
console.log('1초 후');
return delay(1000); // Promise를 반환하면 다음 then이 기다림
})
.then(() => {
console.log('2초 후');
return delay(1000);
})
.then(() => console.log('3초 후'));
.then()이 반환하는 값이 다음 .then()의 인수가 됩니다. 만약 .then()에서 Promise를 반환하면, 다음 .then()은 그 Promise가 완료될 때까지 기다립니다. 이 메커니즘으로 비동기 작업을 순서대로 실행하는 체이닝이 가능합니다. .finally()는 로딩 스피너를 숨기거나, 파일을 닫거나, 연결을 해제하는 정리 작업에 활용합니다.
⚠️ 오류 처리
이 섹션에서는 Promise 체인에서 오류를 안전하게 처리하는 방법을 살펴보겠습니다.
Promise에서 오류는 체인을 따라 내려가 가장 가까운 .catch()에서 처리됩니다. .then() 안에서 throw를 사용해 오류를 발생시키거나, reject()를 호출한 Promise를 반환하면 체인이 .catch()로 건너뜁니다.
// 오류가 catch로 전파됨
Promise.resolve('시작')
.then(val => {
throw new Error('then에서 오류 발생');
})
.then(val => {
console.log('이것은 실행되지 않음'); // 건너뜀
})
.catch(err => {
console.error('잡음:', err.message); // 잡음: then에서 오류 발생
return '복구된 값'; // catch에서 반환하면 체인 계속 가능
})
.then(val => {
console.log(val); // '복구된 값'
});
// Promise 직접 reject
function fetchUser(id) {
return new Promise((resolve, reject) => {
if (!id) {
reject(new Error('ID가 필요합니다'));
return; // 이후 코드 실행 방지
}
setTimeout(() => resolve({ id, name: '홍길동' }), 500);
});
}
fetchUser(0)
.then(user => console.log(user))
.catch(err => console.error(err.message)); // 'ID가 필요합니다'
// 주의: .catch() 없이 reject되면 UnhandledPromiseRejection!
// 모든 Promise 체인 끝에 .catch()를 붙이세요
.catch()에서 값을 반환하면 체인을 계속 이어갈 수 있으므로 오류 상황의 기본값을 설정하거나 복구 로직을 구현할 수 있습니다. 오류를 처리하고 싶지 않으면 .catch()에서 다시 throw err하면 체인이 다시 rejected 상태가 됩니다.
⚡ Promise.all · race · allSettled
이 섹션에서는 여러 Promise를 동시에 처리하는 정적 메서드들을 살펴보겠습니다.
여러 비동기 작업을 순차가 아닌 병렬로 실행하면 전체 대기 시간을 크게 줄일 수 있습니다. 예를 들어 세 개의 API를 순차로 호출하면 3초 걸리지만, 병렬로 호출하면 가장 느린 것 하나의 시간만 걸립니다.
// Promise.all — 모두 성공해야 완료, 하나라도 실패하면 즉시 reject
Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/products').then(r => r.json())
])
.then(([user, orders, products]) => {
// 세 가지 모두 성공했을 때만 실행, 구조 분해로 받음
console.log(user, orders, products);
})
.catch(err => {
// 어느 하나라도 실패하면 여기로
console.error('하나 이상 실패:', err);
});
// Promise.race — 가장 먼저 완료/실패한 것 반환
const fast = new Promise(resolve => setTimeout(() => resolve('빠름'), 500));
const slow = new Promise(resolve => setTimeout(() => resolve('느림'), 2000));
Promise.race([fast, slow]).then(result => {
console.log(result); // '빠름'
});
// 타임아웃 구현에 활용
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('타임아웃')), ms)
);
return Promise.race([promise, timeout]);
}
withTimeout(fetch('/api/slow'), 3000)
.then(res => res.json())
.catch(err => console.error(err.message)); // 3초 초과 시 '타임아웃'
// Promise.allSettled — 모두 완료될 때까지 기다림 (성공/실패 무관)
Promise.allSettled([
Promise.resolve('성공1'),
Promise.reject('실패'),
Promise.resolve('성공2')
]).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('성공:', result.value);
} else {
console.log('실패:', result.reason);
}
});
});
Promise.all()은 모든 작업이 성공해야 하는 경우에 적합합니다. 페이지 초기화 시 여러 API를 병렬로 호출해 모두 받아야 렌더링하는 패턴이 대표적입니다. Promise.race()는 가장 빠른 결과만 필요하거나 타임아웃을 구현할 때 유용합니다. Promise.allSettled()는 일부가 실패해도 나머지 결과가 필요한 경우, 예를 들어 여러 파일 업로드의 개별 성공/실패 현황을 보고할 때 적합합니다.
🔄 콜백 지옥 vs Promise 비교
이 섹션에서는 23강의 콜백 지옥 예시를 Promise로 리팩터링해 차이를 직관적으로 확인해 보겠습니다.
// 콜백 지옥 (23강)
getUser(userId, (err1, user) => {
if (err1) return handleError(err1);
getOrders(user.id, (err2, orders) => {
if (err2) return handleError(err2);
getProduct(orders[0].productId, (err3, product) => {
if (err3) return handleError(err3);
console.log(user, orders, product);
});
});
});
// Promise 체이닝 — 훨씬 읽기 쉬움
getUser(userId)
.then(user => {
return getOrders(user.id)
.then(orders => ({ user, orders }));
})
.then(({ user, orders }) => {
return getProduct(orders[0].productId)
.then(product => ({ user, orders, product }));
})
.then(({ user, orders, product }) => {
console.log(user, orders, product);
})
.catch(err => handleError(err)); // 오류 처리 한 곳에서!
// 25강의 async/await로는 더 간결해짐
async function loadData(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const product = await getProduct(orders[0].productId);
return { user, orders, product };
}
📝 핵심 요약
24강에서 배운 내용을 정리해 보겠습니다.
| 개념 | 설명 |
|---|---|
| Promise 3상태 | Pending → Fulfilled(resolve) / Rejected(reject) |
.then(fn) | 성공 시 처리, 반환값이 다음 then의 인수 |
.catch(fn) | 체인 어디서든 오류를 잡아서 처리 |
.finally(fn) | 성공/실패 무관 항상 실행 |
Promise.all([]) | 모두 성공해야 완료, 하나 실패 시 즉시 reject |
Promise.race([]) | 가장 먼저 완료/실패한 것 반환 |
Promise.allSettled([]) | 전부 완료될 때까지 기다림 |
다음 강인 25강 — async·await에서는 Promise를 동기 코드처럼 작성하는 ES2017 문법을 배웁니다. async function과 await 키워드로 24강의 .then() 체이닝을 훨씬 읽기 쉬운 코드로 바꿀 수 있습니다. 내부적으로는 Promise를 사용하므로, 24강을 완벽히 이해한 지금이 최적의 타이밍입니다!
관련 주제
- Promise 상태
- resolve reject
- then catch finally
- Promise.all
- Promise.race
- Promise.allSettled
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 자바스크립트 기초 30강 — 웹을 움직이는 언어
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
자바스크립트 기초 30강 — 웹을 움직이는 언어
이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
불러오는 중...
