Learning
레슨 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에서도 매우 효과적입니다.