Promise 완전 정복
Promise는 비동기 작업의 미래 결과를 나타내는 객체입니다. 콜백 지옥을 해결하고 더 선언적인 비동기 코드를 작성할 수 있게 해줍니다.
Promise란?
Promise는 세 가지 상태를 가집니다:
pending → 초기 상태, 아직 결과 없음
fulfilled → 작업 성공, 결과값 있음
rejected → 작업 실패, 에러 있음
상태는 한 번만 변경되며 되돌릴 수 없습니다 (pending → fulfilled 또는 pending → rejected).
// Promise 상태 시각화
const p1 = new Promise((resolve, reject) => {
// pending 상태...
setTimeout(() => resolve('성공!'), 1000);
// 1초 후 fulfilled 상태
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('실패!')), 1000);
// 1초 후 rejected 상태
});
console.log(p1); // Promise { <pending> }
Promise 생성
// 기본 생성
const promise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = Math.random() > 0.5;
if (success) {
resolve('작업 성공!'); // fulfilled 상태로 전환
} else {
reject(new Error('작업 실패!')); // rejected 상태로 전환
}
});
// 실용적 예시: HTTP 요청 래핑
function fetchUser(id) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/users/${id}`);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('네트워크 오류'));
xhr.send();
});
}
// 타이머 Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log('시작');
await delay(2000);
console.log('2초 후');
}
.then(), .catch(), .finally()
.then() - 성공 처리
fetchUser(1)
.then(user => {
console.log(user.name);
return user.id; // 다음 .then으로 전달
})
.then(id => {
console.log('ID:', id);
});
// .then(onFulfilled, onRejected) - 두 번째 인수로 에러도 처리 가능
fetchUser(1).then(
user => console.log('성공:', user),
err => console.error('실패:', err) // .catch와 비슷하지만 차이 있음
);
.catch() - 에러 처리
fetchUser(999)
.then(user => processUser(user))
.catch(err => {
// 체인의 어느 곳에서든 발생한 에러를 잡음
if (err.message.includes('404')) {
console.log('사용자를 찾을 수 없습니다');
} else {
console.error('예상치 못한 에러:', err);
}
});
// .then(null, handler)와 동일하지만 .catch가 권장됨
.finally() - 항상 실행
// 성공/실패 상관없이 실행 (정리 작업에 유용)
showLoadingSpinner();
fetchData('/api/large-dataset')
.then(data => displayData(data))
.catch(err => showErrorMessage(err))
.finally(() => {
hideLoadingSpinner(); // 항상 실행
// finally는 값을 변경하지 않음
});
// finally는 Promise 값에 영향을 주지 않음
Promise.resolve(42)
.finally(() => console.log('정리')) // '정리' 출력
.then(val => console.log(val)); // 42 출력 (값 유지)
Promise 체이닝
// 각 .then()은 새로운 Promise를 반환
const result = fetchUser(1)
.then(user => {
console.log('사용자:', user.name);
return fetch(`/api/orders?userId=${user.id}`); // Promise 반환
})
.then(response => response.json()) // 자동으로 Promise 평탄화
.then(orders => {
console.log('주문 수:', orders.length);
return orders.filter(o => o.status === 'active');
})
.then(activeOrders => {
console.log('활성 주문:', activeOrders);
return activeOrders; // 최종 결과
})
.catch(err => {
console.error('에러:', err);
return []; // 기본값 반환으로 체인 계속
});
정적 메서드
Promise.resolve() / Promise.reject()
// 이미 값이 있을 때 Promise로 감싸기
const p1 = Promise.resolve(42);
p1.then(val => console.log(val)); // 42
// 에러 즉시 생성
const p2 = Promise.reject(new Error('즉시 실패'));
p2.catch(err => console.error(err.message)); // 즉시 실패
// 함수 반환 타입 통일에 유용
function getConfig(useCache) {
if (useCache && cachedConfig) {
return Promise.resolve(cachedConfig); // 동기값도 Promise로
}
return fetchConfig(); // 비동기
}
Promise.all() - 모두 완료 대기
// 모든 Promise가 성공해야 성공. 하나라도 실패하면 즉시 실패
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// 병렬 실행으로 성능 향상
console.time('순차');
const a = await fetch('/api/a').then(r => r.json());
const b = await fetch('/api/b').then(r => r.json());
console.timeEnd('순차'); // ~200ms (직렬)
console.time('병렬');
const [c, d] = await Promise.all([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json())
]);
console.timeEnd('병렬'); // ~100ms (병렬)
// 실패 시 동작
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('실패!')),
Promise.resolve(3)
]).catch(err => console.error(err.message)); // '실패!' - 나머지 무시
Promise.race() - 가장 먼저 완료된 것
// 성공이든 실패든 가장 먼저 완료된 Promise의 결과 반환
const fastest = await Promise.race([
fetch('/api/server1').then(r => r.json()),
fetch('/api/server2').then(r => r.json()),
fetch('/api/server3').then(r => r.json())
]);
// 타임아웃 패턴 구현
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`타임아웃: ${ms}ms 초과`)), ms)
);
return Promise.race([promise, timeout]);
}
try {
const data = await withTimeout(fetchLargeData(), 5000);
console.log(data);
} catch (err) {
console.error(err.message); // 5초 초과 시 타임아웃 에러
}
Promise.allSettled() - 모두 완료 (성공/실패 무관)
// 모든 Promise의 결과를 수집 (실패해도 계속)
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999), // 404 에러
fetchUser(2)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${index}: 성공 -`, result.value);
} else {
console.log(`${index}: 실패 -`, result.reason.message);
}
});
// 실전: 여러 API 호출 결과 수집
async function fetchMultipleUsers(ids) {
const results = await Promise.allSettled(
ids.map(id => fetchUser(id))
);
return {
successful: results
.filter(r => r.status === 'fulfilled')
.map(r => r.value),
failed: results
.filter(r => r.status === 'rejected')
.map(r => r.reason)
};
}
Promise.any() - 가장 먼저 성공한 것
// 하나라도 성공하면 그 결과 반환. 모두 실패하면 AggregateError
try {
const result = await Promise.any([
fetch('/api/primary').then(r => r.json()),
fetch('/api/backup1').then(r => r.json()),
fetch('/api/backup2').then(r => r.json())
]);
console.log('첫 번째 성공:', result);
} catch (err) {
if (err instanceof AggregateError) {
console.error('모든 요청 실패:', err.errors);
}
}
// Promise.race vs Promise.any 차이
// race: 첫 번째 settled (성공이든 실패든)
// any: 첫 번째 fulfilled (성공만)
실전 예제
API 병렬 호출 패턴
// 사용자 대시보드 데이터 한 번에 로드
async function loadDashboard(userId) {
const [user, stats, notifications, recentActivity] = await Promise.all([
api.getUser(userId),
api.getUserStats(userId),
api.getNotifications(userId),
api.getRecentActivity(userId)
]);
return {
user,
stats,
notifications,
recentActivity
};
}
// 일부 실패해도 계속 진행
async function loadDashboardRobust(userId) {
const results = await Promise.allSettled([
api.getUser(userId),
api.getUserStats(userId),
api.getNotifications(userId),
api.getRecentActivity(userId)
]);
const [userResult, statsResult, notifResult, activityResult] = results;
return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
stats: statsResult.status === 'fulfilled' ? statsResult.value : {},
notifications: notifResult.status === 'fulfilled' ? notifResult.value : [],
recentActivity: activityResult.status === 'fulfilled' ? activityResult.value : []
};
}
재시도 패턴
function retry(fn, attempts = 3, delay = 1000) {
return new Promise((resolve, reject) => {
function attempt(remaining) {
fn()
.then(resolve)
.catch(err => {
if (remaining <= 1) {
reject(err);
} else {
console.log(`재시도... 남은 횟수: ${remaining - 1}`);
setTimeout(() => attempt(remaining - 1), delay);
}
});
}
attempt(attempts);
});
}
// 사용
retry(() => fetchUnstableAPI(), 3, 2000)
.then(data => console.log('성공:', data))
.catch(err => console.error('3회 시도 후 실패:', err));
Promise 체이닝으로 파이프라인 구축
// 데이터 처리 파이프라인
function processImage(imageUrl) {
return fetch(imageUrl)
.then(response => {
if (!response.ok) throw new Error('이미지 로드 실패');
return response.blob();
})
.then(blob => createImageBitmap(blob))
.then(bitmap => {
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
return canvas;
})
.then(canvas => canvas.toDataURL('image/webp', 0.8))
.catch(err => {
console.error('이미지 처리 실패:', err);
return '/images/placeholder.webp'; // 폴백
});
}
Promise 안티패턴
// 나쁜 예 1: Promise 생성자에서 불필요하게 감싸기 (Promise Constructor Anti-pattern)
// 나쁨
function fetchData() {
return new Promise((resolve, reject) => {
fetch('/api/data')
.then(r => r.json())
.then(resolve)
.catch(reject);
});
}
// 좋음 - 이미 Promise를 반환하면 그대로 반환
function fetchData() {
return fetch('/api/data').then(r => r.json());
}
// 나쁜 예 2: .catch()를 잊어버림
async function riskyOperation() {
fetch('/api/data')
.then(r => r.json())
.then(processData);
// .catch() 없음 → 에러가 조용히 무시됨 (Unhandled Rejection)
}
// 나쁜 예 3: Promise를 await 없이 사용
async function missingAwait() {
const result = fetchData(); // await 없음!
console.log(result); // Promise { <pending> } 출력
}
고수 팁
1. Promise.withResolvers() (ES2024)
// 기존 방식: 외부에서 resolve/reject 접근 불편
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ES2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// 실전 예시
function createDeferred() {
return Promise.withResolvers();
}
const { promise: userPromise, resolve: resolveUser } = createDeferred();
// 나중에 다른 곳에서 resolve 가능
setTimeout(() => resolveUser({ name: 'Alice' }), 1000);
const user = await userPromise;
2. Promise 체이닝에서 값 접근 유지
// 중간 값을 나중에도 쓰고 싶을 때
async function withMultipleValues() {
// 방법 1: async/await
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(user.name, orders.length); // 둘 다 접근 가능
// 방법 2: 객체로 전달
return getUser(1)
.then(user => getOrders(user.id).then(orders => ({ user, orders })))
.then(({ user, orders }) => console.log(user.name, orders.length));
}
3. 동시성 제한 (Concurrency Limiting)
// 한 번에 N개씩만 처리
async function batchProcess(items, processor, concurrency = 5) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
}
return results;
}
// 100개 항목을 5개씩 병렬 처리
const processed = await batchProcess(
Array.from({ length: 100 }, (_, i) => i),
async (item) => {
await delay(100);
return item * 2;
},
5
);