JSON Toolkit

JSON to TypeScript: 인터페이스, 타입 가드, 생성의 한계

핵심 요약: 샘플 JSON에서 인터페이스를 생성하는 것은 절반의 일입니다. 선택 필드, null, 유니언, 날짜, 큰 정수는 사람이 손봐야 하며, 타입은 런타임 검증을 하지 않습니다.

중급약 9분 소요2026-10-31 발행2026-11-04 업데이트
JSON to TypeScript 인터페이스와 타입 가드 가이드 표지

핵심부터 말하면, 타이핑을 아끼기 위해 인터페이스를 생성한 뒤 선택성과 null을 다시 점검하고, 날짜와 큰 정수는 문자열로 유지하며, 경계에 런타임 검증을 더하세요. 생성 타입을 계약으로 취급하면 검증이 전혀 없습니다.

생성기는 붙여 넣은 샘플 하나만 봅니다. 필드가 빠질 수 있는지, 값이 null일 수 있는지, 날짜가 어떤 형식인지는 모두 추측이며, 틀린 추측은 당신이 고쳐야 합니다.

1. 생성기가 실제로 하는 일

JSON 샘플을 JSON to TypeScript에 붙여 넣으면 돌려받는 인터페이스는 모양 요약입니다. 본 키와 값 타입을 세고, 가능한 가장 좁은 표현을 고릅니다.

{
  "id": 1,
  "email": "ada@example.com",
  "profile": { "nickname": "Ada", "age": 36 },
  "tags": ["admin", "beta"],
  "createdAt": "2026-10-31T08:00:00Z"
}

출력은 대략 이렇습니다:

interface User {
  id: number;
  email: string;
  profile: { nickname: string; age: number };
  tags: string[];
  createdAt: string;
}

컴파일은 되지만 이상적인 값을 설명할 뿐, 엔드포인트의 실제 계약은 아닙니다. 생성기는 email이 빠질 수 있는지, tags가 비어 있을 수 있는지, createdAt이 항상 시간대를 담는지 알지 못합니다. 다른 레코드에서 id가 문자열일 수 있다는 것조차 모릅니다.

그래서 올바른 기대는 이것입니다. 생성기는 타이핑을 아끼고, 사람은 의미를 공급합니다.

2. 선택 필드와 null: ?는 | null이 아님

이 부분이 생성 출력에서 가장 자주 복사되고, 가장 자주 깨집니다.

TypeScript 형태 의미 대응하는 JSON
email: string 항상 존재, 항상 문자열 {"email":"a@b.c"}
email?: string 키가 없을 수 있음, 있을 때 null일 수 없음 {} 또는 {"email":"a@b.c"}
email: string | null 키는 항상 존재, 값은 null일 수 있음 {"email":null}
email?: string | null 키가 없을 수 있고 값도 null일 수 있음 세 경우 모두

생성기는 샘플 하나만 봤으므로 보통 첫 번째 형태를 출력합니다. API가 실제로 null을 반환할 수 있다면 타입은 거짓말을 하는 것입니다. 모두 컴파일되고, 그러다 email.toLowerCase()가 런타임에서 터집니다.

고치는 방법은 두 가지입니다:

  1. "키 없음"과 "값이 null"을 모두 덮는 실제 샘플 여러 개를 모아 생성기가 다시 추론하게 합니다.
  2. 생성 후 필드를 손으로 ?: T | null로 바꾸고 코드에서 두 분기를 실제로 처리합니다.

모든 것을 ?: string으로 표시하는 지름길을 택하지 마세요. 선택은 "키가 없을 수 있다"는 뜻이지 "값이 비어 있을 수 있다"는 뜻이 아닙니다. undefined와 null은 JSON.stringify에서도 다르게 동작합니다. 전자는 생략되고, 후자는 null로 유지됩니다.

3. 고르지 않은 배열 요소와 유니언 타입

배열은 가장 틀리기 쉬운 구조입니다. tags가 세 레코드에서 ["admin"], [], ["beta", 3]으로 나타나면, 생성기는 본 샘플에 따라 string[], (string | number)[], unknown[]을 낼 수 있습니다.

사람이 결정해야 합니다:

  • 요소 타입이 정말 섞여 있음: (string | number)[]로 쓰고 사용 지점에서 좁히세요.
  • 한 레코드만 오염됨: 데이터를 고치세요. any[]로 숨기지 마세요.
  • 빈 배열: 생성기가 요소 타입을 추론하지 못하고 never[]나 any[]로 떨어지므로 직접 채워야 합니다.
  • 고정 길이 튜플: 업무가 길이를 진실로 보장할 때만 [string, string]을 쓰세요. 그렇지 않으면 나중에 요소를 더할 때 컴파일 오류가 됩니다.

객체 배열도 키가 고르지 않을 때 주의가 필요합니다:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // some records do not carry this field
}

선택성은 배열이 아니라 요소 타입에 두세요. items?: OrderItem[]는 배열 자체가 없을 수 있다는 뜻이며, 완전히 다른 진술입니다.

4. 날짜, 큰 정수, 열거: JSON에 없는 타입

JSON에는 문자열과 숫자만 있고, 업무 의미는 관례에 삽니다. 생성기는 리터럴에서 추론하므로 손으로 바로잡으세요:

JSON 리터럴 흔한 생성 출력 마땅한 타입
"2026-10-31T08:00:00Z" string 전송 계층에서는 string으로 유지하고 사용 지점에서 변환, Date를 직접 선언하지 말 것
1717075200000 number 밀리초 타임스탬프에는 number를 쓰고 단위를 문서화
"1234567890123456789" string number가 정밀도를 잃지 않도록 string으로 유지
1과 "1" 혼합 number | string 하나의 형태로 정규화, 유니언을 업무 코드로 밀어 넣지 말 것
"admin" string 리터럴 유니언 'admin' | 'editor', 또는 공유 열거 상수
true와 "true" 혼합 boolean | string 이 타입을 생성하지 말고 데이터 소스를 고칠 것

가장 중요한 규칙: JSON의 날짜는 항상 문자열입니다. createdAt: Date로 쓰는 것은 흔한 자기기만입니다. JSON.parse는 결코 Date를 만들지 않으므로 런타임 값은 여전히 문자열이고 .getTime()이 터집니다. 전송 타입은 string으로 두고, 경계에서 new Date(value)를 명시적으로 호출하며, Number.isNaN을 확인하세요.

큰 정수도 같습니다. id가 2^53을 넘으면 JSON.parse가 이미 정밀도를 잃었고, number를 생성하는 것은 버그를 타입에 굳히는 일입니다. 백엔드가 문자열을 반환하고 프런트엔드가 문자열로 유지하는 것이 유일하게 신뢰할 수 있는 선택입니다.

5. 생성 타입은 런타임 검증을 하지 않음

TypeScript 타입은 컴파일 시 지워집니다. 인터페이스가 아무리 정밀해도 {"id": "x"} 응답을 막을 수 없습니다. 그것은 당신의 코드를 제약하지, 네트워크로 도착하는 바이트를 제약하지 않습니다.

/**
 * 런타임 값이 사용자 객체인지 확인합니다.
 * @param value 검증할 런타임 값
 * @returns User의 최소 모양을 만족하는지 여부
 */
function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}

타입 가드는 검사에 적은 필드만 덮습니다. 필드가 늘어나면 손으로 쓴 가드는 유지해야 할 두 번째 정의가 되어 인터페이스에서 멀어집니다.

실용적인 절충은 이렇습니다:

접근 런타임 검증 타입의 원천 적합
인터페이스만 없음 손으로 작성 또는 생성 신뢰하는 내부 데이터, 컴파일 타임 제약
인터페이스 + 손 타입 가드 있음 어긋나는 두 정의 필드가 적고 경계가 명확
zod / valibot 있음 스키마에서 추론한 타입 외부 API, 폼, 설정
JSON Schema + 검증기 있음 스키마에서 생성한 타입 언어 간 계약, 문서

데이터가 외부에서 온다면 스키마를 단일 진실 원천으로 만들고 z.infer나 json-schema-to-ts로 타입을 도출하세요. 그러면 "검증 통과"와 "타입이 올바름"이 같은 사실이 되어 어긋날 두 번째 정의가 없습니다. 전체 양방향 동기화 접근은 스키마와 TypeScript 타입 동기화를 참고하세요.

6. Interface 또는 type: 생성 출력으로 논쟁하지 말 것

생성기는 보통 interface를 내지만, 대부분 상황에서 둘은 상호 교환 가능합니다. 실제로 중요한 것은 세 가지 능력입니다:

  • 선언 병합: interface는 여러 번 선언하면 자동으로 병합되고 type은 안 됩니다. 서드파티 타입을 패치할 때 도움이 되지만, 업무 모델에서는 피해야 합니다. 병합은 "추가 필드"가 컴파일러를 몰래 통과하게 합니다.
  • 유니언과 매핑 타입: type은 A | B, Partial<T>, 템플릿 리터럴 타입을 직접 표현할 수 있고 interface는 안 됩니다.
  • 재귀: 둘 다 지원하지만 복잡한 재귀 모양은 type이 더 자연스럽게 읽힙니다.

더 유용한 팀 규칙: 객체 모양에는 interface, 유니언·유틸리티 타입·함수 타입에는 type을 쓰세요. 생성 출력은 기본값으로 두고, 리뷰 시간은 선택성과 런타임 검증에 쓰세요.

7. 도구 체인: 생성하는 세 가지 방법

접근 입력 장점 한계
이 사이트의 JSON to TypeScript 샘플 JSON 즉시 동작, 로컬 처리 현재 샘플에만 기반, 수동 좁히기 필요
quicktype JSON / JSON Schema 다국어 출력, CLI 통합 보수적 추론, 유니언이 너무 넓을 수 있음
json-schema-to-typescript JSON Schema 계약 주도, 재현 가능 먼저 신뢰할 수 있는 스키마가 필요

전형적인 quicktype 호출:

npx quicktype -s json -o user.ts user.json --just-types

--just-types는 타입만 내고 런타임 변환 코드를 내지 않아, 이미 파싱 계층이 있는 프로젝트에 어울립니다. 스키마를 손으로 쓰거나 백엔드가 생성한다면 json-schema-to-typescript가 더 적합합니다. required, enum, additionalProperties를 존중하기 때문이며, 이 제약은 샘플 추론이 볼 수 없는 것입니다.

스키마 생성에도 자체 함정이 있습니다. 모든 필드를 required로 표시하거나 null을 조용히 선택으로 바꾸는 식입니다. 흔한 JSON Schema 생성기 함정을 참고하세요.

8. 생성 타입을 커밋하기 전 체크리스트

출력을 커밋하기 전 다섯 항목을 점검하세요:

  1. 필드 이름 정규화: userId와 user_id가 공존하지 않게 하고 변환 계층에서 한 번 매핑하세요.
  2. 선택성 재점검: 실제 응답과 비교해 어떤 필드가 빠지거나 null일 수 있는지 찾으세요.
  3. 날짜와 id는 문자열로 유지: 단위와 형식을 타입에 문서화하고 사용 지점에서 명시적으로 파싱하세요.
  4. any와 unknown 줄이기: 생성기가 any를 낸 곳마다 타입을 더하거나 unknown으로 두고 가드를 더하세요.
  5. 런타임 검증 추가: 외부 데이터는 최소 한 번 검증하세요. 인터페이스가 나쁜 데이터를 잡아 줄 거라 기대하지 마세요.

프로젝트가 여러 JSON 라이브러리(JSON.parse, superjson, lossless-json)를 쓴다면 큰 정수와 날짜에 대한 동작이 다르므로 선택 시 확인하세요. JSON 파싱 라이브러리 선택을 참고하세요.

생성기가 왜 모든 선택 필드를 required로 표시하나?

샘플 하나만 봤으므로 나타난 모든 키를 항상 존재하는 것으로 취급합니다. 다른 경우를 덮는 여러 샘플을 제공하거나 JSON Schema에서 생성하세요. 그곳의 required 배열이 권위 있는 원천입니다.

인터페이스에 Date를 선언하면 왜 런타임에서 실패하나?

JSON.parse는 문자열, 숫자, 불리언, null, 객체, 배열만 만들고 결코 Date를 만들지 않습니다. Date 선언은 컴파일 타임 약속인데 런타임 값은 문자열로 남습니다. 전송 계층에서는 string으로 두고 경계에서 변환하세요.

생성 타입을 검증으로 쓸 수 있나?

아니요. TypeScript 타입은 컴파일 후 지워지며 런타임 데이터에 아무것도 부과하지 않습니다. 외부 데이터는 zod, valibot, ajv 또는 손으로 쓴 타입 가드로 검증하세요.

큰 정수 id를 number로 타입해야 하나 string으로 해야 하나?

2^53을 넘을 수 있다면 반드시 string입니다. JSON.parse가 파싱 중 정밀도를 잃고, number로 타입하면 버그를 더 이상 고칠 수 없는 곳에 써 넣는 셈입니다.