본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

프로미스 — 아직 도착하지 않은 결과

~13 min · async, promises, mental-model

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"프로미스는 나중에 채울 빈칸이 아니라, 이미 시작된 작업의 성공이나 실패를 지켜보는 손잡이야."

프로미스의 세 가지 상태

프로미스는 대기 중(pending), 성공(fulfilled), 실패(rejected) 가운데 한 상태에 있어. 작업이 끝나기 전에는 대기 중이고, 끝난 뒤에는 값과 함께 성공하거나 오류 이유와 함께 실패해.

상태 변화는 한 번뿐이야. 대기 중이던 프로미스가 성공이나 실패로 정해지면 다시 돌아가거나 결과를 바꾸지 않아. 이 성질 덕분에 같은 프로미스를 여러 곳에서 지켜봐도 모두 같은 완료 결과를 받게 돼.

결과에 반응하는 세 메서드

.then은 성공했을 때, .catch는 실패했을 때, .finally는 어느 쪽이든 작업이 끝났을 때 실행할 처리를 붙여:

fetchUser(id)
  .then(user => user.email)
  .then(email => sendWelcome(email))
  .catch(err => logError(err))
  .finally(() => closeDB())

이 메서드들은 원래 프로미스를 바꾸지 않고 새 프로미스를 돌려줘. .then의 처리 함수가 일반 값을 반환하면 다음 프로미스는 그 값으로 성공해. 다른 프로미스를 반환하면 그 작업이 끝날 때까지 기다리고, 오류를 던지면 다음 프로미스는 실패해. 이렇게 앞 단계의 결과가 다음 단계로 자연스럽게 이어져.

오류는 처리하는 지점까지 이어져

중간 단계에서 오류가 나면 성공 처리 함수들은 건너뛰고 가장 가까운 .catch로 이동해. 콜백마다 if (err)를 반복하지 않아도 되는 이유야. 체인의 끝에 둔 .catch 하나가 앞 단계 어느 곳에서 난 오류든 받을 수 있어.

반대로 실패를 아무도 받지 않으면 처리되지 않은 프로미스 거부가 돼. Node 15부터 기본 설정에서는 처리되지 않은 거부가 잡히지 않은 예외로 바뀌어 프로세스를 끝내. process.on('unhandledRejection', ...)은 마지막 기록과 정리를 위한 안전망으로 둘 수 있지만, 거기서 정상 실행을 회복하려고 해서는 안 돼. 각 체인을 .catch로 마무리하거나, async 함수 안에서 try/catch로 실패의 책임을 분명히 해.

여러 프로미스를 함께 기다리는 방법

  • Promise.all은 모두 성공해야 할 때 써. 하나라도 실패하면 곧바로 실패해.
  • Promise.allSettled는 전부 끝날 때까지 기다린 뒤 각 작업의 성공과 실패를 함께 돌려줘.
  • Promise.race는 성공이든 실패든 가장 먼저 끝난 결과를 따라가.
  • Promise.any는 가장 먼저 성공한 값을 돌려주고, 전부 실패했을 때만 실패해.

어느 방법이 맞는지는 “한 작업의 실패가 전체를 멈춰야 하는가?”라는 질문으로 고르면 돼. 필요한 실패 정책을 먼저 정하면 메서드는 자연스럽게 따라와.

직접 프로미스를 만들 일은 많지 않아

요즘 Node 표준 라이브러리는 fs/promises, dns/promises, timers/promises처럼 프로미스를 바로 돌려주는 API를 제공해. 예전 콜백 API는 util.promisify로 감쌀 수도 있어. 그래서 new Promise(...)는 이벤트 기반 API를 한 번의 완료 결과로 바꾸거나, 기존 도구가 없는 경계를 연결할 때 주로 사용해.

프로미스를 직접 만드는 코드가 자주 반복된다면 표준 라이브러리나 이미 검증된 도우미가 같은 일을 제공하는지 먼저 확인해. 프로미스 생성자 안에서 예외와 취소, 중복 완료를 모두 다루는 일은 생각보다 까다로워.

Pippa의 고백

처음에는 프로미스를 “나중에 값을 돌려주는 함수”라고 생각했어. 아빠가 프로미스는 계산을 시작하는 함수가 아니라 이미 진행 중인 작업의 완료를 바라보는 객체라고 바로잡았지. const p = doThing()에서 작업은 호출과 함께 시작되고, p는 그 결과를 붙잡는 손잡이야. 이 구분을 이해하자 연속된 .then도 명령 목록이 아니라 앞선 결과에 대한 반응으로 보이기 시작했어.

Code

콜백 피라미드를 프로미스 체인으로 펴기·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);
  }
}
실패 정책에 맞는 프로미스 조합 고르기·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

사용자 ID 배열을 받아 사용자 객체 배열을 돌려주는 fetchAllUsers(ids)를 세 방식으로 만들어 봐. 첫째는 for...ofawait로 하나씩 호출하고, 둘째는 Promise.all로 함께 호출하며, 셋째는 잘못된 ID 하나가 전체를 막지 않도록 Promise.allSettled를 써. 같은 입력으로 시간을 재고 실패 결과가 어떻게 달라지는지 기록해.
Hint
각 호출에 200ms 지연을 넣으면 열 건을 순차 실행할 때는 약 2초, 함께 실행할 때는 약 200ms가 걸려. allSettled도 비슷한 시간에 끝나지만 실패한 ID를 따로 확인할 수 있어. 선택 기준은 한 건의 실패가 전체 묶음을 실패시켜야 하는지야.

Progress

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

댓글 0

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

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