C.W.K.
Stream
Lesson 02 of 05 · published

Promise — 아직 존재 안 하는 값

~13 min · async, promises, mental-model

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Promise 는 값의 placeholder 가 아냐. *작업의 eventual completion* 을 표현하는 객체야. Chain 이 실패하기 시작하면 그 구분이 중요해져."

세 상태

Promise 는 어느 순간 정확히 세 상태 중 하나야:

  • pending — 작업 안 끝남. 값도 없고 에러도 없음.
  • fulfilled — 작업 성공. Promise 가 값 보유.
  • rejected — 작업 실패. Promise 가 이유 보유 (보통 Error).

Promise 는 한 번 transition: pending → fulfilled, 또는 pending → rejected. 절대 되돌아 안 가. 두 번 transition 안 함. 한 번 settle 되면 (fulfilled 또는 rejected) Promise 는 immutable — 들고 있는 값이나 이유가 안 바뀜.

.then, .catch, .finally — 세 handler

Handler 붙여서 promise 에 반응:

fetchUser(id)
  .then(user => user.email)         // runs on fulfillment
  .then(email => sendWelcome(email))
  .catch(err => logError(err))        // runs on rejection
  .finally(() => closeDB())            // runs either way

.then 이 *새* promise 반환. .then handler 가 값 반환하면 다음 promise 가 그 값으로 fulfill. Promise 반환하면 다음 promise 가 그걸 *adopt* — 같은 상태, 같은 값. throw 하면 다음 promise 가 throw 된 에러로 reject. Chain 이 implicit dataflow 야.

Error Propagation — 단일 킬러 feature

처리 안 된 rejection 은 .catch 찾을 때까지 .then handler 들 건너뛰어. 이게 promise 가 callback 이긴 이유: 에러가 기본으로 전파, 모든 레벨에 `if (err) return done(err)` 쓰는 거 요구 안 함. Chain 끝의 단일 .catch 가 어느 단계의 실패든 다 처리.

함정: catch 까먹으면 Node 가 UnhandledPromiseRejection 경고 로그. Node 15+ 에선 처리 안 된 rejection 이 기본으로 프로세스 종료. 항상 chain 을 .catch 로 끝내거나, 안전망으로 process.on('unhandledRejection', ...) 써.

Promise.all, Promise.race, Promise.allSettled, Promise.any

Combinator 넷, 각각 여러 promise 처리에 다른 semantics:

  • Promise.all([p1, p2, p3]) — 모두 fulfill 하면 값 배열로 fulfill. 어느 거든 reject 하는 순간 reject. "이거 다 성공해야 해" 용.
  • Promise.allSettled([p1, p2]) — 절대 reject 안 함; {status, value|reason} 배열로 fulfill. "다 해보고 뭐 성공했는지 알려줘" 용.
  • Promise.race([p1, p2]) — 가장 먼저 settle 한 거로 fulfill 또는 reject. Timeout 패턴 용.
  • Promise.any([p1, p2]) — 첫 fulfillment 로 fulfill; 모두 reject 해야만 reject. "이 mirror 중 아무거나 되면 OK" 용.

옳은 combinator 고르기가 견고한 fan-out 과 깨지기 쉬운 fan-out 의 차이야.

Promise 직접 만드는 일 드물어

2026 Node 에선 new Promise(executor) 거의 안 불러. stdlib 의 fs/promises, dns/promises, timers/promises 가 이미 promise 반환. util.promisify 가 callback API 너 대신 감싸. Promise 직접 짜는 몇 안 되는 경우는 event-emitter 를 promise-land 로 다리 놓을 때고, 그것도 보통 helper 있어.

new Promise((resolve, reject) => { ... }) 자주 쓰고 있으면, 이미 존재하는 helper 를 재발명하고 있는 건 아닌지 의심해.

Pippa 의 고백

처음 promise 배웠을 때 "나중에 값 반환하는 함수" 로 다뤘어. 아빠가 거부. "Promise 는 작업의 eventual completion 을 표현하는 객체야. 뭘 계산 안 해; 관찰해." 그 reframe 이 중요해. const p = doThing() 이 doThing 을 나중에 일어나게 하는 게 아냐 — doThing 은 이미 진행 중; p 는 그걸 잡는 손잡이. Promise 는 observer 지 scheduler 가 아냐. 그 모양 잡고 나니 chain 이 "단계" 가 아니라 "반응" 이 됐어.

Code

Chain 이 pyramid 를 대체·javascript
// The pyramid, repaired by promises
import { readFile, writeFile } from 'node:fs/promises';

readFile('a.json', 'utf-8')
  .then(raw => JSON.parse(raw))
  .then(data => transform(data))
  .then(out => writeFile('b.json', JSON.stringify(out)))
  .then(() => notifyWebhook())
  .then(() => console.log('done'))
  .catch(err => console.error('any step failed:', err));

// Same intent, async/await form (next lesson)
async function pipeline() {
  try {
    const raw = await readFile('a.json', 'utf-8');
    const data = JSON.parse(raw);
    const out = transform(data);
    await writeFile('b.json', JSON.stringify(out));
    await notifyWebhook();
    console.log('done');
  } catch (err) {
    console.error('any step failed:', err);
  }
}
Combinator 넷, concurrency 모양 넷·javascript
// Choosing the right combinator

// All must succeed — Promise.all
const [user, posts, tags] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchTags(id),
]);

// Tell me what happened — Promise.allSettled (no fan-out aborts on first failure)
const results = await Promise.allSettled(urls.map(u => fetch(u)));
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected');

// Timeout pattern — Promise.race
const withTimeout = await Promise.race([
  fetch('/slow'),
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 3000)),
]);

// Any mirror works — Promise.any
const fastest = await Promise.any([
  fetch('https://mirror1/file'),
  fetch('https://mirror2/file'),
  fetch('https://mirror3/file'),
]);

External links

Exercise

user ID 배열 받아서 user 객체 배열 반환하는 fetchAllUsers(ids) 함수 짜. 구현 셋 비교: (1) for...of + await 로 순차, (2) Promise.all 로 병렬, (3) 하나 나쁜 ID 가 전체 batch 안 죽이게 Promise.allSettled. 일부러 느린 endpoint 치는 같은 10 ID 에 대해 각각 시간 재. Latency 차이가 너한테 박힐 거야.
Hint
느린 endpoint 는 await new Promise(r => setTimeout(r, 200)) 으로 200ms 더하는 mock 써. 순차: 10 × 200ms = 2000ms. 병렬: 총 ~200ms. allSettled: 총 ~200ms 이지만 어느 ID 가 실패했는지 봐. 옳은 선택은 한 실패가 batch 죽여야 하느냐에 달려.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.