레슨 7 / 8·25분
실전: 타입 안전한 API 클라이언트
프로젝트 개요
TypeScript의 타입 시스템을 활용해 타입 안전한 API 클라이언트를 만듭니다. 제네릭, Discriminated Union, 유틸리티 타입을 실전에서 사용합니다.
- •제네릭 기반의 타입 안전한 fetch 래퍼
- •API 엔드포인트별 요청/응답 타입 정의
- •에러 처리를 위한 Result 타입 패턴
- •Discriminated Union으로 상태 관리
Result 타입 패턴
typescript
// Result 타입 — 성공/실패를 타입으로 표현
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
function success<T>(data: T): Result<T> {
return { ok: true, data };
}
function failure<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// 사용
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return failure("0으로 나눌 수 없습니다");
return success(a / b);
}
const result = divide(10, 3);
if (result.ok) {
console.log(result.data); // number 타입 확정
} else {
console.error(result.error); // string 타입 확정
}API 타입 정의
typescript
// API 타입 시스템
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// 엔드포인트별 타입 매핑
interface ApiEndpoints {
"/users": {
GET: { response: User[] };
POST: { body: Omit<User, "id">; response: User };
};
"/users/:id": {
GET: { response: User };
PUT: { body: Partial<User>; response: User };
DELETE: { response: void };
};
"/posts": {
GET: { response: Post[] };
POST: { body: Omit<Post, "id">; response: Post };
};
}타입 안전한 Fetch 래퍼
typescript
const BASE_URL = "https://api.example.com";
async function apiClient<T>(
url: string,
options?: RequestInit,
): Promise<Result<T>> {
try {
const res = await fetch(BASE_URL + url, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) {
return failure(new Error("HTTP " + res.status));
}
const data: T = await res.json();
return success(data);
} catch (err) {
return failure(err instanceof Error ? err : new Error(String(err)));
}
}
// 타입이 보장되는 API 호출 함수들
async function getUsers(): Promise<Result<User[]>> {
return apiClient<User[]>("/users");
}
async function getUser(id: number): Promise<Result<User>> {
return apiClient<User>("/users/" + id);
}
async function createUser(
data: Omit<User, "id">,
): Promise<Result<User>> {
return apiClient<User>("/users", {
method: "POST",
body: JSON.stringify(data),
});
}
// 사용
async function main() {
const result = await getUsers();
if (result.ok) {
result.data.forEach(user => {
console.log(user.name); // 자동완성 지원!
});
}
}💡
Result 패턴을 사용하면 try-catch 없이도 에러를 타입 수준에서 처리할 수 있습니다. Rust의 Result 타입에서 영감을 받은 패턴으로, TypeScript에서도 매우 효과적입니다.