[Javascript] Promise
정의
Promise 는 미래의 어떤 시점에 결정될 값을 나타내는 객체다. ECMAScript 2015 (ES6) 에 도입되어 콜백 기반 비동기의 가독성과 에러 처리 문제를 해결했다.
“프로미스는 자바스크립트 비동기 처리에 사용되는 객체”, MDN
전체 메커니즘과 발전사는 비동기와 타이밍, 콜백부터 async/await까지의 발전사 글 참조.
세 가지 상태
| 상태 | 의미 | 들어가는 방법 |
|---|---|---|
| pending | 대기, 아직 결정되지 않음 | new Promise(...) 직후 |
| fulfilled | 이행 (성공) | executor 가 resolve(value) 호출 |
| rejected | 거부 (실패) | executor 가 reject(reason) 호출 또는 throw |
핵심: 한 번 fulfilled/rejected 되면 다시 바뀌지 않는다 (settled 상태). 이 불변성이 콜백 대비 가장 큰 안전성 이점.
기본 사용
const p = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5
? resolve('성공')
: reject(new Error('실패'));
}, 1000);
});
p
.then((value) => console.log(value))
.catch((err) => console.error(err))
.finally(() => console.log('done'));
executor 함수는 동기적으로 즉시 실행 된다. resolve / reject 는 비동기 작업 완료 시 호출되는 콜백.
인스턴스 메서드
| 메서드 | 시그니처 | 언제 호출? |
|---|---|---|
.then(onFulfilled, onRejected?) | 두 콜백 | fulfilled 시 1번, rejected 시 2번 |
.catch(onRejected) | 한 콜백 | rejected 시 |
.finally(onFinally) | 인자 없는 콜백 | 정착 시 무조건 |
모두 새 Promise 를 반환 하여 체이닝 가능.
정적 메서드, 동시성 네 형제
| 메서드 | fulfilled 시 | rejected 시 | 결과 |
|---|---|---|---|
Promise.all(iter) | 모두 fulfilled | 하나라도 rejected | 값 배열 / 첫 사유 |
Promise.allSettled(iter) | 모두 정착 | 절대 안 됨 | {status, value/reason} 배열 |
Promise.race(iter) | 가장 먼저 fulfilled | 가장 먼저 rejected | 가장 먼저 정착한 값/사유 |
Promise.any(iter) | 하나라도 fulfilled | 모두 rejected | 첫 fulfilled / AggregateError |
사용 예시
// 모두 필요
const [users, posts] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/posts').then(r => r.json()),
]);
// 부분 실패 OK
const results = await Promise.allSettled([f1(), f2(), f3()]);
results.filter(r => r.status === 'fulfilled').forEach(/* ... */);
// 타임아웃
await Promise.race([
fetch('/slow'),
new Promise((_, rj) => setTimeout(() => rj('timeout'), 3000)),
]);
// 미러 중 하나라도
await Promise.any([
fetch('https://mirror1/x'),
fetch('https://mirror2/x'),
]);
체이닝 메커니즘
.then 의 콜백이 반환하는 값에 따라 다음 단계의 입력이 결정된다.
| 반환 | 다음 then 입력 |
|---|---|
| 일반 값 | 그 값 |
| Promise | 그 Promise 가 정착할 때 값 |
| Thenable | Promise 처럼 풀어서 처리 |
| 예외 throw | rejected Promise |
이 자동 unwrap 이 .then().then().then() 의 평탄한 흐름을 가능하게 한다.
함정과 한계
.then 두 번째 인자 vs .catch
Promise.resolve('hi').then(
(v) => { throw new Error('oops'); },
(err) => console.log('이건 잡힐까?'), // ❌ 안 잡힘
);
onRejected 는 이전 단계 의 에러만 잡는다. 자기 짝 onFulfilled 에서 던진 에러는 못 잡는다.
TIP
.catch 가 안전. 이전 모든 단계의 에러를 한 번에 잡는다.
Promise 도 지옥이 될 수 있다
.then 안에서 또 .then 을 중첩하면 콜백 지옥의 사촌. 항상 평탄하게 펴자. 또는 async/await 사용.
unhandled rejection
.catch 없이 rejected 된 Promise 는 Node.js 에서 unhandledRejection 이벤트로, 브라우저에선 콘솔 경고로 노출. 운영 환경에서 누수의 신호.
process.on('unhandledRejection', (reason, promise) => {
console.error('처리 안 된 rejection:', reason);
});
resolve 가 Promise 면 자동 추적
new Promise((resolveOuter) => {
resolveOuter(
new Promise((resolveInner) => setTimeout(resolveInner, 1000))
);
});
// 외부 Promise 는 즉시 resolveOuter 호출됐지만,
// 그 안의 내부 Promise 가 정착할 때까지 fulfilled 되지 않음.
실무 예시
콜백 API 를 Promise 화
function promisify(fn) {
return (...args) => new Promise((resolve, reject) => {
fn(...args, (err, result) => {
err ? reject(err) : resolve(result);
});
});
}
// Node.js 의 callback 스타일 fs.readFile 을 Promise 로
const readFile = promisify(fs.readFile);
const data = await readFile('config.json');
Node.js 는 빌트인 util.promisify, fs/promises 모듈로 표준 제공.
Promise.withResolvers (ES2024)
executor 밖에서 resolve / reject 를 노출하고 싶을 때.
const { promise, resolve, reject } = Promise.withResolvers();
externalEmitter.once('data', resolve);
externalEmitter.once('error', reject);
return promise;
상태 전이 다이어그램
stateDiagram-v2
[*] --> pending: new Promise 생성
pending --> fulfilled: resolve 호출
pending --> rejected: reject 호출 또는 throw
fulfilled --> [*]
rejected --> [*]
한 번 fulfilled 또는 rejected 로 정착하면 다시 바뀌지 않는다. 이 불변성이 콜백 대비 가장 큰 안전성 이점.
마이크로태스크 큐와의 관계
Promise 의 .then / .catch / .finally 콜백은 마이크로태스크 큐 에 들어간다. 일반 태스크 (setTimeout 등) 보다 먼저 실행된다.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 출력 순서: 1, 4, 3, 2
실행 순서:
- 동기 코드:
1,4 - 마이크로태스크:
3(Promise.then) - 매크로태스크:
2(setTimeout)
IMPORTANT
Promise 체인이 길면 마이크로태스크가 쌓여 렌더링이 블로킹될 수 있다. 긴 체인은 setTimeout(0) 으로 분리 고려.
에러 처리 패턴
중앙 집중 에러 처리
async function withErrorHandling(fn, fallback) {
try {
return await fn();
} catch (err) {
console.error('Error:', err.message);
return fallback;
}
}
const data = await withErrorHandling(
() => fetch('/api/data').then(r => r.json()),
[]
);
재시도 패턴
async function retry(fn, times = 3, delay = 1000) {
for (let i = 0; i < times; i++) {
try {
return await fn();
} catch (err) {
if (i === times - 1) throw err;
await new Promise(r => setTimeout(r, delay * (i + 1)));
}
}
}
const result = await retry(() => fetch('/api/unstable'));
타임아웃 래퍼
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
const data = await withTimeout(fetch('/slow-api'), 5000);
실행 순서 보장
sequenceDiagram
participant JS as JS엔진
participant Micro as 마이크로태스크
participant Macro as 매크로태스크
JS->>JS: 동기 코드 실행
JS->>Micro: Promise.then 등록
JS->>Macro: setTimeout 등록
JS->>Micro: 마이크로태스크 전부 소진
JS->>Macro: 다음 매크로태스크 1개
자세한 이벤트 루프 동작은 js-event-loop, js-microtask-queue 참조.
참고
이 글의 용어 (5개)
- [Javascript] 이벤트 루프javascript
- 정의 이벤트 루프 (Event Loop) 는 싱글 스레드 JavaScript 엔진이 비동기 작업을 처리하기 위해 돌리는 무한 루프다. 콜 스택이 비어있을 때마다 마이크로태스크 큐…
- [Javascript] 콜백javascript
- 정의 콜백 (callback): 다른 함수의 인자로 넘겨져, 받은 쪽(caller)이 적절한 시점에 호출하는 함수. 특성이 있어야 가능한 패턴이다. JavaScript 에서는 함…
- [Javascript] async/awaitjavascript
- 정의 / 는 기반 비동기 코드를 마치 동기 코드처럼 쓸 수 있게 해주는 ES2017 의 문법 설탕. 본질은 Promise 그 자체, 문법만 다르다. 전체 동작 메커니즘은 글 참조…
- [Javascript] Microtask Queuejavascript
- 정의 Microtask Queue (마이크로태스크 큐) 는 이벤트 루프 가 매 틱마다 완전히 비울 때까지 처리하는 우선순위 큐다. 매크로태스크 큐보다 항상 먼저 처리된다. ECM…
- [Javascript] Promise.all / race / any / allSettledjavascript
- 정의 의 정적 메서드 4 형제. 여러 Promise 를 동시에 다루는 도구. | 메서드 | fulfilled 조건 | rejected 조건 | 결과 | |:---|:---|:--…
이 개념을 다룬 위키 페이지 (14)
- wikiAsynchronous (비동기)
- wiki[Koa] Koa vs Express
- wiki[Framework] Koa.js
- wiki[JavaScript] AbortController
- wiki[Javascript] async/await
- wiki[Javascript] 콜백
- wiki[Javascript] 콜백 지옥
- wiki[Javascript] Error / try-catch
- wiki[Javascript] 이벤트 루프
- wiki[JavaScript] fetch API: 현대 HTTP 클라이언트
- wiki[Javascript] Microtask Queue
- wiki[Javascript] Promise.all / race / any / allSettled
- wiki[Node.js] 런타임 개요
- wiki[Frontend] SPA Architecture: CSR, SSR, SSG, hydration
💬 댓글