[TypeScript] API 명세서를 통해 타입 설계하기

ujinsim·2025년 5월 7일
0
post-thumbnail
  1. API 명세읽기
  2. string으로 표현하기, 타입 이름은 해당 분야로 짓기
  3. string 지양하기 → 구체적인 타입으로 좁히기
  4. 공식 명칭에는 상표 붙이기

예제 1: 가상의 사용자 정보 API

API 엔드포인트: https://api.example.com/users

응답 예시:

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "zipcode": "12345"
  },
  "phone": "123-456-7890",
  "website": "https://example.com"
}

1. string으로 일단 만들기

// Address 타입 정의
interface Address {
  street: string;
  city: string;
  zipcode: string;
}

// User 타입 정의
interface User {
  id: number;
  name: string;
  email: string;
  address: Address;
  phone: string;
  website: string;
}

API 호출 함수

fetch를 사용하여 데이터를 호출하고 타입을 적용합니다.

async function fetchUsers(): Promise<User[]> {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error("Failed to fetch users");
  }
  return await response.json();
}

2. string을 지양하기

: 이메일과 웹사이트URL 전화번호 타입 구체화 하기

// 이메일 타입 정의
type EmailAddress = `${string}@${string}.${string}`;

// 웹사이트 URL 타입 정의
type WebsiteURL = `http${"s" | ""}://${string}.${string}`;

// 전화번호 타입 정의
type PhoneNumber = `${number}-${number}-${number}`;

// Address 타입 정의
interface Address {
  street: string;
  city: string;
  zipcode: `${number}`; // 우편번호를 숫자로 제한
}

// User 타입 정의
interface User {
  id: number;
  name: string;
  email: EmailAddress; // 엄격한 이메일 형식
  address: Address;
  phone: PhoneNumber; // 간단한 전화번호 형식
  website: WebsiteURL; // 웹사이트 URL 형식
}

올바른 값

const user: User = {
  id: 1,
  name: "John Doe",
  email: "john.doe@example.com",
  address: {
    street: "123 Main St",
    city: "Anytown",
    zipcode: "12345"
  },
  phone: "123-456-7890",
  website: "https://example.com"
};

잘못된 값 (컴파일 오류 발생)

const invalidUser: User = {
  id: 2,
  name: "Jane Doe",
  email: "jane.doe[at]example.com", // 오류: 이메일 형식이 아님
  address: {
    street: "456 Elm St",
    city: "Othertown",
    zipcode: "abcde" // 오류: 숫자 형식이 아님
  },
  phone: "1234567890", // 오류: 전화번호 형식이 아님
  website: "example.com" // 오류: URL 형식이 아님
};

4. 브랜딩하기

: 있으면 하자 !

그렇다면 any와 unknown은 명세서와 타입 설계시 언제 사용하나요 ?

any, unknown 특징

특징unknownany
안전성타입을 사용하기 전에 검사 필수모든 작업 허용 (타입 검사 없음)
타입 검사엄격 (타입을 좁혀야만 사용 가능)느슨함 (타입 체크가 무시됨)
IntelliSense타입 좁히기 후 사용 가능IntelliSense 거의 없음
사용 시기외부 데이터, 알 수 없는 타입빠른 프로토타이핑 또는 유연성이 필요할 때

언제 unknown을 사용할까?

: unknown은 타입 안전성을 유지해야 하지만 데이터의 구체적인 타입을 사전에 알 수 없는 경우에 적합합니다.

  1. 외부에서 오는 데이터 (API 응답 구조가 문서화되지 않았거나 동적으로 변할 가능성이 있을 때.)
async function fetchData(url: string): Promise<unknown> {
  const response = await fetch(url);
  return response.json();
}

async function processData() {
  const data: **unknown**= await fetchData("https://api.example.com/data");

  // 타입 좁히기 필요
  if (Array.isArray(data)) {
    console.log(`Array of length: ${data.length}`);
  } else if (typeof data === "object" && data !== null) {
    console.log(`Object with keys: ${Object.keys(data)}`);
  } else {
    console.log("Unknown data type");
  }
}
  • 장점: 타입을 검사하기 전까지는 데이터를 조작할 수 없으므로, 실수를 방지할 수 있습니다.
  • 단점: 추가적인 타입 검사 코드가 필요합니다.
  1. 안전하게 다뤄야 할 데이터가 있을 때

unknown은 강제로 타입을 좁혀야 하기 때문에 더 안전하게 데이터를 사용할 수 있습니다.

function processValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // 안전
  } else if (typeof value === "number") {
    console.log(value.toFixed(2)); // 안전
  } else {
    console.log("Unsupported type");
  }
}

언제 any를 사용할까?

: any는 타입 안전성을 무시하고 빠르게 코드를 작성해야 할 때 사용합니다. 하지만 남용을 피해야 하며, 가능한 빨리 정확한 타입으로 대체하는 것이 좋습니다.

  1. 빠른 프로토타이핑

개발 초기 단계에서 타입 정의가 아직 명확하지 않거나, 많은 데이터를 테스트해야 할 때 유용합니다.

function logData(data: any) {
  console.log(data);
}

logData({ id: 1, name: "John Doe" });
logData("This is a string");
logData(42);
  • 장점: 유연성.
  • 단점: 모든 작업이 허용되므로, 타입 오류를 놓칠 가능성이 큽니다.
  1. 타입 검사가 필요 없는 경우

특정 로직이 타입에 구애받지 않고 모든 데이터 타입을 다뤄야 할 때 사용합니다.

function mergeObjects(obj1: any, obj2: any): any {
  return { ...obj1, ...obj2 };
}
  • 예: 데이터 직렬화/역직렬화, JSON.parse(), 로깅.

unknown vs any 사용 사례 비교

const jsonData: **any** = JSON.parse('{"id": 1, "name": "John"}');
console.log(jsonData.toUpperCase()); // 런타임 오류 발생 가능 (TypeScript가 경고하지 않음)

const unknownData: **unknown** = JSON.parse('{"id": 1, "name": "John"}');
if (typeof unknownData === "string") {
  console.log(unknownData.toUpperCase()); // 비교적 안전
} else {
  console.log("Data is not a string");
}
  • any: 런타임 오류 가능성이 있음.
  • unknown: 타입 검사 후 사용 가능, 타입 안전성 증가.

예제 2: 상표가 있는 API

{
  "id": "12345",
  "name": "Apple MacBook Pro",
  "category": "laptop",
  "price": 2499.99,
  "currency": "USD",
  "availability": {
    "inStock": true,
    "estimatedDeliveryDays": 5
  },
  "rating": {
    "average": 4.8,
    "reviews": 1245
  }
}

1. API 명세 읽고 타입 설계 (string 중심)

우선, API 데이터 구조를 단순히 string 및 기타 기본 타입으로 표현합니다.

interface Product {
  id: string; // 제품 ID
  name: string; // 제품 이름
  category: string; // 제품 카테고리
  price: number; // 제품 가격
  currency: string; // 통화 단위
  availability: {
    inStock: boolean; // 재고 여부
    estimatedDeliveryDays: number; // 예상 배송일
  };
  rating: {
    average: number; // 평균 평점
    reviews: number; // 리뷰 수
  };
}

2. string으로 표현된 타입 이름을 해당 분야로 지정

타입 이름을 구체화하여 해당 데이터와 연관된 도메인 용어를 반영합니다.

type ProductID = string;
type ProductName = string;
type ProductCategory = string;
type Currency = string;

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}

이 단계에서는 단순히 의미를 부여한 별칭을 도입했습니다. 타입 자체는 여전히 string 기반입니다.

3. string 지양하고 구체적인 타입으로 좁히기

이제 더 구체적인 타입으로 좁힙니다. 예를 들어:

  • Currency는 ISO 4217 표준 코드로 한정합니다.
  • ProductCategory는 정해진 카테고리 값으로 제한합니다.
type ProductID = `${number}`; // ID는 숫자 기반 문자열
type ProductName = string;

type ProductCategory = "laptop" | "smartphone" | "tablet" | "accessory"; // 카테고리 제한

type Currency = "USD" | "EUR" | "JPY"; // ISO 4217 통화 코드

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}

이 단계에서는 string 대신 구체적인 값으로 제한하여 데이터 구조의 타입 안전성을 향상시켰습니다.

4. 공식 명칭에 상표(branding) 붙이기

마지막으로, 상표를 추가하여 타입의 고유성을 강화합니다. 상표를 추가하면 다른 동일 구조의 타입과 구별할 수 있습니다.

type ProductID = `${number}` & { _brand: "ProductID" };
type ProductName = string & { _brand: "ProductName" };

type ProductCategory = "laptop" | "smartphone" | "tablet" | "accessory" & { _brand: "ProductCategory" };

type Currency = "USD" | "EUR" | "JPY" & { _brand: "Currency" };

interface Product {
  id: ProductID;
  name: ProductName;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
  rating: {
    average: number;
    reviews: number;
  };
}
  • 이제 ProductIDCurrency는 동일한 구조의 다른 string과 구별됩니다.
  • 타입 간 혼동을 방지하고, 잘못된 데이터 사용 가능성을 줄입니다.
const macbook: Product = {
  id: "12345" as ProductID,
  name: "Apple MacBook Pro" as ProductName,
  category: "laptop" as ProductCategory,
  price: 2499.99,
  currency: "USD" as Currency,
  availability: {
    inStock: true,
    estimatedDeliveryDays: 5,
  },
  rating: {
    average: 4.8,
    reviews: 1245,
  },
};

퀴즈 ‼️

아래의 Product 타입은 특정 API 명세를 기반으로 설계되었습니다. 다음 중 올바르지 않은 부분은 무엇일까요

type ProductID = `${number}` & { _brand: "ProductID" };
type ProductCategory = "laptop" | "smartphone" | "tablet" | "accessory" & { _brand: "ProductCategory" };
type Currency = "USD" | "EUR" | "JPY" & { _brand: "Currency" };

interface Product {
  id: ProductID;
  name: string;
  category: ProductCategory;
  price: number;
  currency: Currency;
  availability: {
    inStock: boolean;
    estimatedDeliveryDays: number;
  };
}
const productB: Product = {
  id: "102" as ProductID,
  name: "Apple iPad",
  category: "tablet" as ProductCategory,
  price: 599.99,
  currency: "GBP" as Currency,
  availability: {
    inStock: false,
    estimatedDeliveryDays: 7,
  },
};
  • currency가 GBP로 설정되어 있습니다. Currency 타입은 "USD", "EUR", "JPY"만 허용합니다.
profile
프론트엔드 공부 중인 학생입니다. 💻👩‍🎤

0개의 댓글