C
JavaScript/비동기/Lesson 16

Promise — *비동기의 표준*

1시간·theory
이 챕터
2/3

Promise — *비동기의 표준*

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ Pending → Fulfilled / Rejected 상태 전이
  • ✅ .then / .catch / .finally 체이닝
  • ✅ Promise.all · race · any · allSettled 차이

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

Promise 가 뭐냐

핵심 한 줄

Promise = "지금은 결과가 없지만 나중에 줄게 약속한" 객체. 비동기 작업의 표준. 콜백 지옥을 해결한 2015년 ES6 의 큰 진보.

콜백 지옥 — 옛 방식

javascript
// 🧪 가짜 API — 콜백 스타일 (Node.js 옛 표준)
const fetchUser   = (id,   cb) => setTimeout(() => cb(null, { id, name: '홍' }), 50);
const fetchOrders = (uid,  cb) => setTimeout(() => cb(null, [{ id: 1 }, { id: 2 }]), 50);
const fetchItems  = (oid,  cb) => setTimeout(() => cb(null, [{ name: '책' }]), 50);
const handle = (err) => console.error('실패:', err);
const id = 42;

fetchUser(id, (err, user) => {
    if (err) return handle(err);
    fetchOrders(user.id, (err, orders) => {
        if (err) return handle(err);
        fetchItems(orders[0].id, (err, items) => {
            if (err) return handle(err);
            console.log(items);   // [{ name: '책' }]
            // 들여쓰기가 *오른쪽으로* 무한 증가 → 콜백 지옥
        });
    });
});

콜백 안에 콜백 안에 콜백... 피라미드처럼 들여쓰기 증가. 콜백 지옥.

Promise 방식

javascript
// 🧪 가짜 API — Promise 스타일 (모던 표준)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser   = (id)  => wait(50).then(() => ({ id, name: '홍' }));
const fetchOrders = (uid) => wait(50).then(() => [{ id: 1 }, { id: 2 }]);
const fetchItems  = (oid) => wait(50).then(() => [{ name: '책' }]);
const handle = (err) => console.error('실패:', err);
const id = 42;

fetchUser(id)
    .then(user   => fetchOrders(user.id))
    .then(orders => fetchItems(orders[0].id))
    .then(items  => console.log(items))   // 📤 [{ name: '책' }]
    .catch(err   => handle(err));

// 💡 콜백 지옥의 들여쓰기 → 수평적 체이닝. 에러도 끝에서 1번 catch.

체이닝으로 수평적. 에러도 맨 끝 catch 한 번 으로 처리.

Promise 의 3 상태

javascript
const success = true;
const value = 42;

const p = new Promise((resolve, reject) => {
    // 1️⃣ pending (대기 중) — 처음 생성된 순간
    if (success) {
        resolve(value);   // 2️⃣-A fulfilled (성공) → then 으로 받음
    } else {
        reject(new Error('실패!'));  // 2️⃣-B rejected (실패) → catch 로 받음
    }
});

p.then(v   => console.log('성공:', v))     // 성공 시 실행
 .catch(err => console.error('실패:', err)) // 실패 시 실행
 .finally(() => console.log('끝!'));         // 성공/실패 무관 항상 실행

// 📤 출력:
//   성공: 42
//   끝!

한 번 fulfilled 또는 rejected 되면 변경 불가.

자주 만나는 Promise

javascript
// fetch — 표준 HTTP
fetch('/api/users/42')
    .then(res => res.json())
    .then(data => console.log(data));

// setTimeout 의 Promise 버전
const wait = (ms) => new Promise(r => setTimeout(r, ms));
wait(1000).then(() => console.log("1초 후"));

// Promise.resolve — 이미 완료된 Promise
const p = Promise.resolve(42);
p.then(v => console.log(v));   // 42

병렬 — Promise.all

javascript
// 🧪 가짜 API (복붙 가능하게)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser    = (id) => wait(50).then(() => ({ id, name: '홍' }));
const fetchOrders  = (id) => wait(50).then(() => [{ orderId: 1 }]);
const fetchProfile = (id) => wait(50).then(() => ({ bio: '개발자' }));

(async () => {
    // 🏎️ 세 요청 "동시" 시작 → 모두 끝날 때까지 대기 → 결과 배열로 받음
    const [user, orders, profile] = await Promise.all([
        fetchUser(42),     // 시작 시각: 0ms
        fetchOrders(42),   // 시작 시각: 0ms  ← 같이 출발!
        fetchProfile(42)   // 시작 시각: 0ms
    ]);
    // 끝나는 시각: max(각 요청 소요시간)

    console.log(user);     // { id: 42, name: '홍' }
    console.log(orders);   // [{ orderId: 1 }]
    console.log(profile);  // { bio: '개발자' }
})();

세 요청 동시 시작 → 모두 완료 시 결과 반환. 직렬보다 3배 빠름.

주의: 하나라도 실패하면 전체 실패. 부분 실패 허용 이면 Promise.allSettled:

javascript
const results = await Promise.allSettled([...]);
// [{status:'fulfilled', value:...}, {status:'rejected', reason:...}]

Promise.race — 가장 빠른 하나

javascript
// 🧪 가짜 응답 — 빠른(50ms) vs 느린(200ms)
const wait = (ms, val) => new Promise(r => setTimeout(() => r(val), ms));

(async () => {
    // 🏁 race = 경주. 둘 중 "먼저 끝난 것" 만 결과로 받음
    const fastest = await Promise.race([
        wait(50,  'cached'),    // 빠름 → 이김
        wait(200, 'origin'),    // 느림 → 짐
    ]);
    console.log(fastest);   // 'cached'   ← 50ms 응답이 먼저

    // 💡 응용 — 타임아웃 구현
    const withTimeout = (promise, ms) => Promise.race([
        promise,
        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms))
    ]);

    try {
        const slow = wait(500, '느린응답');
        const res = await withTimeout(slow, 100);   // 100ms 안에 응답 없으면 timeout
        console.log(res);
    } catch (e) {
        console.error(e.message);   // "timeout"   ← 500 > 100 이라 시간초과
    }
})();

흔한 함정

1. catch 누락:

javascript
fetch('/api').then(handle);   // ❌ 에러 무시
fetch('/api').then(handle).catch(err => log(err));   // ✅

처리 안 된 Promise rejection 은 Unhandled Promise Rejection 경고. 항상 catch.

2. then 안에서 또 then:

javascript
// ❌ Promise 가 중첩
fetch('/api')
    .then(res => {
        res.json().then(data => console.log(data));
    });

// ✅ return 으로 체이닝
fetch('/api')
    .then(res => res.json())
    .then(data => console.log(data));

3. await 대신 then 남용:
요즘은 async/await 가 표준. then 체이닝은 간단한 경우만.

한 번 정리

  • Promise = 미래 결과 약속하는 객체
  • then·catch·finally 체이닝
  • Promise.all 로 병렬
  • 모던 코드는 async/await 으로 더 깔끔하게

⚡ 직접 해보기 — Promise 체이닝 + Promise.all

순차 체이닝과 병렬 Promise.all 을 직접 비교.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 .then 체인을 async/await 로 바꿔줘"
  • "이 코드의 unhandled rejection 가능성을 .catch / try-catch 로 막아줘"
  • "이 fetch 들을 Promise.all 로 병렬화해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

먼저 읽으면 좋은 개념: 이벤트 루프
다음 추천: Async/Await
Promise 프로미스 - JavaScript