본문으로 건너뛰기
김신건의 로그

[Javascript] Promise

· 수정 · 📖 약 2분 · 946자/단어 #javascript #async #ecmascript
Promise, promise, 프로미스, JavaScript Promise, JS 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 가 정착할 때 값
ThenablePromise 처럼 풀어서 처리
예외 throwrejected 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. 동기 코드: 1, 4
  2. 마이크로태스크: 3 (Promise.then)
  3. 매크로태스크: 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 조건 | 결과 | |:---|:---|:--…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기