API 응답을 다루는 TypeScript 개발에서 피할 수 없는 것이 JSON에 대응하는 타입 정의입니다. 깊게 중첩된 JSON을 눈으로 좇으며 interface를 손으로 쓰는 것은 지루할 뿐 아니라 오타의 온상이 되기도 합니다.

JSON에서 TypeScript 타입을 자동 생성하는 방법: 실제 예제로 배우는 변환 요령

이 글에서는 실제 JSON이 어떤 TypeScript 타입이 되는지를 변환 예제로 보여주면서, 자동 생성의 요령과 주의점을 해설합니다.

기본: 평탄한 객체의 변환

먼저 단순한 예부터 보겠습니다.

{
  "id": 42,
  "name": "Alice",
  "isActive": true
}

이것은 다음과 같은 타입이 됩니다. 각 값의 타입(number / string / boolean)은 실제 값에서 기계적으로 추론할 수 있습니다.

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

중첩과 배열을 포함한 변환

실제 API 응답에는 객체 중첩과 배열이 포함됩니다.

{
  "id": 42,
  "name": "Alice",
  "roles": ["admin", "editor"],
  "profile": {
    "age": 30,
    "city": "Tokyo"
  }
}

배열은 요소의 타입에서 string[]처럼, 중첩된 객체는 별도의 interface로 분리하는 것이 정석입니다.

interface Profile {
  age: number;
  city: string;
}

interface User {
  id: number;
  name: string;
  roles: string[];
  profile: Profile;
}

빠지기 쉬운 포인트

null과 선택적 프로퍼티

JSON에 null이 들어 있는 경우, 그 값만으로는 “항상 null이 될 수 있는지”, “마침 null이었는지”는 판단할 수 없습니다.

{ "nickname": null, "deletedAt": null }

기계적으로 변환하면 nickname: null이 되지만, 실무에서는 string | null이나 nickname?: string이 맞는 경우가 대부분입니다. 생성 후 API 사양에 맞춰 손으로 보정하는 것이 현실적인 운용입니다.

interface User {
  nickname: string | null;   // 값이 올 수도 있음
  deletedAt?: string;        // 필드 자체가 없을 수도 있음
}

문자열로 감싼 숫자에 주의

"price": "1980"처럼 숫자가 문자열로 감싸진 API는 드물지 않습니다. 생성 도구는 이를 string으로 추론합니다. 앱에서 계산한다면 타입을 number로 바꾸거나 받은 뒤 변환할지 방침을 정해 두세요.

빈 배열은 타입이 정해지지 않습니다

"tags": []처럼 빈 배열에서는 요소 타입을 추론할 수 없어 never[]any[]가 되기 쉽습니다. 요소가 있는 샘플을 쓰거나, 생성 후 string[] 등으로 손수 지정하세요.

자동 생성을 사용하는 절차

  1. 브라우저 Network 탭이나 curl 출력에서 실제 응답을 복사합니다
  2. JSON → TypeScript 타입 생성 도구에 붙여넣습니다
  3. 루트 interface 이름을 Root에서 UserResponse 등으로 변경합니다
  4. null이나 선택적 필드를 사양에 맞춰 조정합니다
  5. 프로젝트에 붙여넣으면 완료

요점은 가능한 한 실제에 가깝고 필드가 갖춰진 샘플을 쓰는 것입니다. 샘플이 빈약하면 선택적 필드나 타입의 흔들림을 놓칩니다.

타입만으로 부족할 때는 Zod로

TypeScript의 타입은 ‘컴파일 시점’의 검사입니다. 외부 API가 실제로 타입대로 데이터를 반환한다는 보장은 없습니다. 런타임에도 데이터 형태를 검증하고 싶다면 JSON → Zod 스키마 생성으로 검증 스키마를 만들어 schema.parse(data)로 입구 검사를 넣는 것을 권장합니다. Zod는 타입 추론도 되므로 타입과 검증을 한곳에서 관리할 수 있습니다.

자주 묻는 질문

생성된 타입의 루트 이름을 매번 변경하는 게 번거롭습니다

많은 도구가 루트를 Root라는 범용 이름으로 출력합니다. 이는 복사 후 프로젝트의 명명 규칙(ApiResponse, UserProfile 등)으로 일괄 치환하는 것이 편합니다. 중첩된 interface 이름은 내용에 맞는 이름인 경우가 많으므로, 루트만 고치면 충분한 경우가 대부분입니다.

snake_case API를 camelCase로 쓰고 싶습니다

생성되는 타입은 JSON 키를 그대로 반영하므로 created_at 같은 이름이 됩니다. 앱 전체를 camelCase로 통일하고 싶다면 API 경계에서 변환하는(매핑 함수나 camelize 계열 라이브러리) 것이 정석입니다. 타입 정의는 API 실체에 맞추고, 변환은 명시적으로 하면 혼란이 없습니다.

TypeScript 타입과 Zod 스키마, 어느 쪽을 먼저 만들어야 하나요?

런타임 검증이 필요하다면 Zod 스키마를 만들고 z.infer로 타입을 도출하는 방법이 한곳에서 관리할 수 있어 권장됩니다. 타입 검사만으로 충분한 내부 데이터라면 TypeScript 타입 생성만으로도 괜찮습니다. 외부 API나 폼 입력처럼 “신뢰할 수 없는 입력”에는 Zod를 쓰면 안전합니다.