C
TypeScript/비동기/Lesson 03

Promise<T> — TypeScript 로 비동기 안전하게

30분·theory
이 챕터
3/7
TypeScript

Promise — TypeScript 로 비동기 안전하게

💡 왜 배워야 할까요? — JS Promise 에서 한 단계 위로

🎯 JS Promise 는 `.then(user => ...)` 에서 `user` 가 무엇인지 모릅니다 — IDE 자동완성 X, 오타 시 런타임에서야 폭발.
💼 TS Promise 는 `T` 자리에 '안에 담길 값의 타입' 을 박아둡니다 — IDE 가 미리 알려주고, 잘못된 사용은 컴파일이 거부합니다.
실무 API 함수 90% 가 Promise 를 반환합니다. 타입이 박힌 Promise 는 호출부 코드의 신뢰성을 결정합니다.
🔗 async/await, Promise.all 같은 도구도 타입이 박힐 때 진짜 가치가 나옵니다 (Promise.all 은 튜플 타입까지 추론).
🏢 실무에서는
Next.js·React 의 Server Component, Server Action, API 라우트는 전부 async 함수입니다. 반환 타입을 명시하지 않으면 호출부에서 매번 `as User` 같은 강제 캐스팅이 생기고, 그게 누적되면 타입 시스템이 의미를 잃습니다. 실무에서는 Promise, Promise, Promise 처럼 반환 타입을 명시하는 게 기본입니다.

Promise — 제네릭으로 '담길 값' 의 타입을 박는다

Promise 의 T 가 무엇인가

JS Promise 는 '나중에 도착할 값을 약속하는 객체' 입니다. TS Promise 는 거기에 '어떤 타입의 값이 도착할지' 까지 컴파일러에게 알려주는 것입니다.

ts
Promise<string>   // 나중에 string 이 담겨 옴
Promise<User>     // 나중에 User 객체가 담겨 옴
Promise<User[]>   // 나중에 User 배열이 담겨 옴
Promise<void>     // 나중에 끝나기는 하는데, 받을 값은 없음

T 가 박히면 무엇이 바뀌나

ts
const 피자주문 = (): Promise<string> => { ... };

피자주문().then((피자) => {
  //         ^^^^ ← IDE 가 이 자리에서 string 으로 추론
  피자.toUpperCase(); // ✅ string 메서드, 자동완성 됨
  피자.toFixed(2);    // ❌ string 에 toFixed 없다 — 컴파일 거부
});

JS 였다면 피자.toFixed(2) 는 컴파일 통과 → 실행 시 TypeError → 운 좋게 테스트가 잡아주거나, 운 나쁘면 프로덕션에서 터집니다.

async 함수의 반환 타입은 자동으로 Promise 로 감싸진다

ts
async function getUser(): Promise<User> {
  return { id: 1, name: '홍길동' }; // ← User 를 return 하지만
}                                    //   반환 타입은 Promise<User>

async 키워드가 붙은 함수는 무조건 Promise 를 반환합니다. 그래서 반환 타입에 Promise<...> 를 적어야 합니다 (또는 추론에 맡깁니다).

타입 명시 vs 타입 추론

실무에서는 둘 다 씁니다:

ts
// 명시 — 함수 시그니처를 읽기만 해도 의도가 보임 (공개 API 에 권장)
const fetchUser = (id: number): Promise<User> => { ... };

// 추론 — 짧은 콜백·내부 헬퍼에 적합
const delay = (ms: number) =>
  new Promise<void>((r) => setTimeout(r, ms));
// 반환 타입: Promise<void> 로 추론됨

Promise.all 은 튜플 타입까지 추론한다

ts
const [피자, 콜라, 감튀] = await Promise.all([
  주문('🍕'),  // Promise<string>
  주문('🥤'),  // Promise<string>
  주문('🍟'),  // Promise<string>
]);
// 👆 TS 는 [string, string, string] 튜플로 추론
// 만약 타입이 섞이면 [string, number, User] 처럼 정확히 잡아냄

JS 에서는 결과 배열을 받아도 각 원소의 타입을 알 수 없습니다. TS 는 위치별 타입을 보존합니다.

💻 🅰️ 기존 JS 방식 (타입 정보 없음)
// ============================================
// ❌ JS 방식 — IDE 가 user, orders, items 의 타입을 모름
// ============================================

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: '홍길동' }), 300);
  });
}

function fetchOrders(uid) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 1, item: '책' }]), 300);
  });
}

fetchUser(42)
  .then((user) => {
    // user.name?  user.id?  user.email?  IDE 자동완성 없음
    console.log(user);
    return fetchOrders(user.id);
  })
  .then((orders) => {
    // orders 가 배열인지 객체인지 IDE 가 모름
    // orders[0].item 오타 시 런타임에서야 발견
    console.log(orders);
  });

// 위험 포인트:
// - user.naem (오타) → 컴파일 통과, 실행 시 undefined
// - orders.item   (배열인데 객체처럼 접근) → 런타임 undefined
// - fetchUser('42') (id 에 문자열) → 컴파일 통과, 동작은 묘하게 어긋남
💻 🅱️ TypeScript 방식 (Promise · interface · 제네릭)
// ============================================
// ✅ TS 방식 — 타입이 명시되어 IDE 가 매 단계 도와줌
// 실행: tsx promise-demo.ts
// ============================================

// 예제 1: 가장 간단한 Promise
const 피자주문 = (): Promise<string> => {
  //              ↑ 반환 타입: Promise<string>
  return new Promise<string>((resolve) => {
    //                ↑ Promise 가 담을 값의 타입
    console.log('📞 주문 전화함');
    setTimeout(() => resolve('🍕'), 1000);
  });
};

피자주문().then((피자: string) => {
  //                ↑ 받는 값 타입
  console.log('받음:', 피자);
});

// 예제 2: 체이닝 (타입 추론 활용)
const 주문 = (음식: string, 시간: number): Promise<string> =>
  new Promise<string>((resolve) => setTimeout(() => resolve(음식), 시간));

주문('🍕 피자', 500)
  .then((음식) => {              // 자동 추론: string
    console.log('1️⃣ 받음:', 음식);
    return 주문('🥤 콜라', 500);
  })
  .then((음식) => {
    console.log('2️⃣ 받음:', 음식);
    return 주문('🍟 감튀', 500);
  })
  .then((음식) => console.log('3️⃣ 받음:', 음식));

// 예제 3: 실패 처리 (에러 타입 명시)
const 주문하기 = (성공할까: boolean): Promise<string> => {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      if (성공할까) resolve('🍕 피자 도착!');
      else reject(new Error('😭 가게가 문 닫음'));
    }, 500);
  });
};

주문하기(false)
  .then((피자) => console.log('성공:', 피자))
  .catch((에러: Error) => console.log('❌ 실패:', 에러.message));

// 예제 4: 실무 코드 (interface 로 타입 정의)
interface User {
  id: number;
  name: string;
}
interface Order {
  id: number;
  item: string;
}

const wait = (ms: number): Promise<void> =>
  new Promise((r) => setTimeout(r, ms));

const fetchUser = (id: number): Promise<User> =>
  wait(300).then(() => ({ id, name: '홍길동' }));

const fetchOrders = (uid: number): Promise<Order[]> =>
  wait(300).then(() => [{ id: 1, item: '책' }]);

fetchUser(42)
  .then((user: User) => {
    // user.name?  user.id?  ← IDE 자동완성 OK
    // user.naem  ← 컴파일이 즉시 거부 (이 줄에 빨간 줄)
    return fetchOrders(user.id);
  })
  .then((orders: Order[]) => {
    // orders[0].item ← 추론됨
  });

// 예제 5: async/await (TS 추천 방식)
const 작업 = (이름: string, ms: number): Promise<string> =>
  new Promise((r) => setTimeout(() => r(이름), ms));

const 실행 = async (): Promise<void> => {
  const a: string = await 작업('A', 200);
  const b: string = await 작업('B', 200);
  const c: string = await 작업('C', 200);
  console.log(a, b, c);
};

// 예제 6: Promise.all — 튜플 타입으로 추론
const [피자, 콜라, 감튀]: [string, string, string] = await Promise.all([
  주문('🍕', 1000),
  주문('🥤', 1000),
  주문('🍟', 1000),
]);
// 👆 TS 는 위치별 타입을 보존 — 섞이면 [string, number, User] 처럼 정확히

// 예제 7: 제네릭 Promise 헬퍼 (어떤 타입이든 받음)
const delay = <T>(value: T, ms: number): Promise<T> =>
  new Promise((r) => setTimeout(() => r(value), ms));

const 숫자: number = await delay(42, 300);              // T = number
const 문자열: string = await delay('안녕', 300);          // T = string
const 객체: { name: string } = await delay({ name: '홍길동' }, 300);

💡 💡 JS ↔ TS 핵심 차이

1. new Promise() 에는 항상 <T> 를 박아라

ts
new Promise<string>((resolve) => ...) // resolve 가 string 만 받게 강제
new Promise((resolve) => ...)         // T = unknown 으로 추론 (위험)

2. async 함수의 반환 타입은 명시하는 게 좋다

공개 API 함수는 시그니처만 봐도 의도가 드러나야 합니다.

ts
async function getUser(id: number): Promise<User> { ... }

3. Promise.all 의 결과는 튜플로 받아라

ts
const [user, orders] = await Promise.all([fetchUser(1), fetchOrders(1)]);
// user: User, orders: Order[] ← 자동

4. catch 의 에러는 unknown 이다 (TS 4.4+)

ts
try { ... } catch (err) {
  if (err instanceof Error) console.log(err.message); // ✅ 타입 좁히기
  // err.message 바로 접근 ← ❌ unknown 에는 message 없음
}

5. 제네릭 헬퍼는 한 번 만들면 평생 쓴다

ts
const delay = <T>(value: T, ms: number): Promise<T> =>
  new Promise((r) => setTimeout(() => r(value), ms));
// number, string, User 든 다 받음 — 호출부에서 타입 보존

⚡ 직접 실행해보기 — Promise (타입을 뗀 실행 버전)

위 🅱️ TS 코드에서 **타입 표기만 떼어낸 실행 가능 버전**입니다. 런타임 동작은 같아요 — 차이는 IDE 가 잡아주냐 vs 실행 시점에 터지냐 입니다. 💡 진짜 TS 컴파일러로 돌려보고 싶다면 → [TypeScript Playground](https://www.typescriptlang.org/play) 에 위 🅱️ 코드를 붙여넣으세요.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

TypeScript 에서 `Promise<User>` 의 `<User>` 가 의미하는 것은?
💡 `Promise<T>` 의 `T` 는 **resolve 될 때 전달되는 값의 타입**입니다. 그래서 `.then((value) => ...)` 의 `value` 가 자동으로 `T` 로 추론됩니다. reject 측 에러는 TS 의 타입 시스템 한계로 별도 표기가 없고, `catch` 에서 `unknown` 으로 받아 좁혀 써야 합니다.
Promise<T> — JS vs TS 차이 - TypeScript