비동기 + Promise<T> — async 반환 타입
async/await 가 타입과 어떻게 어울리는지. fetch 결과 안전하게 받기.
JS 의 비동기(15편) 위에 TS 가 더하는 한 가지 — "이 비동기가 반환하는 값의 타입". Promise<User> 같은 표기로 컴파일 시점에 결과 모양이 보장됩니다. 15편은 async 반환 타입, fetch 결과 타이핑, catch 의 unknown 까지.
Promise<T> — 미래의 값 타입
// Promise 직접 만들기 — 거의 안 씀 (라이브러리 내부용)
const p: Promise<number> = new Promise((resolve, reject) => {
setTimeout(() => resolve(42), 100);
});
p.then(v => v + 1); // v: number
// 실전 — 대부분 async 함수가 자동으로 Promise 반환
async function getNumber(): Promise<number> {
return 42;
}
// ^ 반환 타입은 Promise<number>
// 본문에서 return 42 (number) 하지만 자동 wrap
async 함수 — 반환 타입의 규칙
// 명시
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// 추론에 맡김 (권장 — 라이브러리 export 만 명시)
async function loadUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<User>;
}
// ^? Promise<User>
// 에러 케이스 — null 가능성
async function findUser(id: number): Promise<User | null> {
const u = await db.findOne({ id });
return u ?? null;
}
await — Promise 를 풀어주기
// await 의 타입은 Awaited<T>
const u: User = await loadUser(1);
// ^ Promise<User> 의 await → User
// 중첩 Promise 도 자동으로 한 번에
async function nested(): Promise<Promise<number>> {
return Promise.resolve(Promise.resolve(42));
}
const n = await nested(); // n: number (이중 Promise 자동 평탄화)
await 의 한 줄 의미. "이 Promise 가 풀릴 때까지 기다린 다음 값을 꺼낸다". 반환 타입에 Promise 가 보이면 await 해서 풀어야 함. 안 풀고 .toFixed() 같은 거 호출하면 컴파일 에러.
fetch 결과 타이핑 — 외부 입력 안전하게
// fetch 의 res.json() 은 Promise<any> — 위험!
async function loadUserUnsafe(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json(); // any 가 자동으로 User 로 — 검증 없이
}
// 안전 패턴 1 — 사용자 정의 타입 가드
async function loadUserGuard(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw: unknown = await res.json();
if (!isUser(raw)) throw new Error("형식 이상");
return raw;
}
// 안전 패턴 2 — zod (실전 표준)
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw = await res.json();
return UserSchema.parse(raw); // throw if invalid
}
Promise.all · allSettled · race · any
// 모두 성공해야 — 하나 실패하면 즉시 reject
const [users, posts] = await Promise.all([
fetch("/users").then(r => r.json()),
fetch("/posts").then(r => r.json()),
]);
// 타입: [any, any] — 위 안전 패턴 적용해 좁히기
// 모두 끝날 때까지 (실패도 받음)
const results = await Promise.allSettled([p1, p2, p3]);
for (const r of results) {
if (r.status === "fulfilled") console.log(r.value);
else console.error(r.reason);
}
// 가장 먼저 settle 된 거 (성공/실패 무관)
const winner = await Promise.race([p1, p2]);
// 가장 먼저 성공 (실패는 무시)
const first = await Promise.any([p1, p2]);
| 메서드 | 언제 | 실패 처리 |
|---|---|---|
| all | 모두 결과 필요 | 하나 실패 → 전체 실패 |
| allSettled | 결과·실패 모두 확인 | 개별 reason |
| race | 가장 빠른 응답·타임아웃 | 먼저 실패해도 결과 |
| any | 하나만 성공하면 OK (fallback) | 모두 실패 → AggregateError |
catch 에서 unknown
try {
await loadUser(1);
} catch (err) {
// 기본 — err: unknown (strict 의 useUnknownInCatchVariables)
if (err instanceof Error) {
console.error(err.message);
} else {
console.error("이상한 것:", err);
}
}
// 헬퍼로 깔끔하게
function isError(x: unknown): x is Error {
return x instanceof Error;
}
function getMsg(x: unknown): string {
return isError(x) ? x.message : String(x);
}
비동기 + 제네릭 — API 클라이언트 패턴
async function api<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw = await res.json();
return schema.parse(raw);
}
// 사용 — 한 함수로 모든 엔드포인트
const user = await api("/api/users/1", UserSchema); // user: User
const posts = await api("/api/posts", z.array(PostSchema)); // posts: Post[]
타임아웃 패턴 — race 활용
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
),
]);
}
const user = await withTimeout(loadUser(1), 5000);
// ES2024 표준 — AbortSignal.timeout
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
async 에서 던진 에러는 자동으로 reject. async 함수 안에서 throw new Error() 하면 호출자의 await 에서 throw 됩니다. 일반 함수처럼 try/catch 로 받으면 끝.
16편 — 데코레이터 (고급 시작)
class·method·property 데코레이터, 메타 정보의 메타 도구.
📚 쉽게 배우는 타입스크립트 교재
이전: 14편 타입 가드 · 현재: 15편 (중급 마지막) · 다음 → 16편 데코레이터 (고급 시작) · 진행: 15/20
이전: 14편 타입 가드 · 현재: 15편 (중급 마지막) · 다음 → 16편 데코레이터 (고급 시작) · 진행: 15/20