모든 TypeScript 개발자는 이런 상황을 겪습니다. 새 API를 연동하는 중이고 샘플 응답은 있는데, 비즈니스 로직을 한 줄도 쓰기 전에 타입화된 인터페이스가 필요한 상황입니다. 이런 인터페이스를 손으로 작성하는 것은 지루하고 반복적이며 실수도 잦습니다 —— 특히 응답이 깊게 중첩되어 있거나 필드가 수십 개일 때 그렇습니다. 이 글은 JSON을 TypeScript 인터페이스로 변환하는 모든 실용적인 접근법을 다룹니다. 출력이 어때야 하는지, 어떻게 자동 생성하는지, 그리고 까다로운 경우를 어떻게 처리하는지.
왜 JSON 데이터에 TypeScript 인터페이스가 중요한가
타입 주석 없이 TypeScript에서 API를 호출하면, 파싱된 응답은 any가 됩니다. 즉 자동완성도 없고, 타입 체크도 없고, 컴파일 타임 안전성도 없습니다. 속성명 오타가 빌드 오류 대신 런타임 버그가 됩니다.
// 타입 없이 —— 모두 any
const response = await fetch('/api/users/42');
const user = await response.json(); // user: any
console.log(user.naem); // 오타 —— 컴파일 시 오류 없음
// 타입화된 인터페이스로
interface User {
id: number;
name: string;
email: string;
}
const response = await fetch('/api/users/42');
const user: User = await response.json();
console.log(user.naem); // Error: Property 'naem' does not exist on type 'User'인터페이스는 런타임 비용이 전혀 없습니다 —— 오직 컴파일 타임에만 존재합니다. 하지만 개발 경험을 완전히 바꿉니다.
JSON에서 TypeScript로의 변환 결과
모든 JSON 객체는 TypeScript interface가 되고, 모든 JSON 원시 타입은 해당 TypeScript 타입으로 매핑되며, 배열은 요소 타입을 추론합니다.
// 입력 JSON
{
"id": 42,
"name": "Alice Johnson",
"email": "alice@example.com",
"active": true,
"score": 98.5,
"address": {
"street": "123 Main St",
"city": "New York",
"zip": "10001"
},
"tags": ["admin", "editor"],
"orders": [
{ "id": 101, "total": 49.99, "status": "shipped" },
{ "id": 102, "total": 99.00, "status": "pending" }
]
}// 출력 TypeScript
interface Root {
id: number;
name: string;
email: string;
active: boolean;
score: number;
address: Address;
tags: string[];
orders: Order[];
}
interface Address {
street: string;
city: string;
zip: string;
}
interface Order {
id: number;
total: number;
status: string;
}각 중첩 객체는 이름이 있는 인터페이스가 됩니다. 이름은 부모 키에서 옵니다 —— address는 Address가 되고, orders의 항목은 Order가 됩니다. 덕분에 타입이 읽기 쉽고 조합 가능해집니다.
타입 매핑 규칙
JSON 타입이 TypeScript 타입에 어떻게 매핑되는지 이해하면 출력이 예측 가능해지고 자신 있게 조정할 수 있습니다.
| JSON 값 | TypeScript 타입 |
|---|---|
"hello" | string |
42, 3.14 | number |
true, false | boolean |
null | null |
{} | 이름 있는 interface |
['a', 'b'] | string[] |
[1, 'two'] | (number | string)[] |
[](빈) | unknown[] |
한 가지 미묘한 점: TypeScript에는 정수 타입이 없습니다. 42와 3.14 모두 number가 됩니다. 이 구분이 중요하다면 브랜드 타입을 사용하거나 주석을 추가하세요.
방법 1: 온라인 JSON-to-TypeScript 변환기 사용
일회성 작업 —— 새 API 엔드포인트, 빠른 프로토타입 —— 에는 온라인 도구가 가장 빠릅니다. fixjson의 JSON-to-TypeScript 변환기는 브라우저에서 전체 변환을 처리합니다.
- JSON 붙여넣기—입력 패널에. 도구는 유효한 JSON뿐 아니라 후행 쉼표나 작은따옴표 같은 흔한 문제가 있는 깨진 JSON도 받아 —— 변환 전에 먼저 수정합니다.
- TypeScript 인터페이스가 즉시 표시—오른쪽에 —— 버튼 클릭 불필요.
- 출력 복사해서 TypeScript 파일에 바로 붙여넣습니다.
모든 작업이 로컬에서 실행됩니다. 어떤 데이터도 서버로 전송되지 않습니다 —— 다루는 JSON에 API 키, 사용자 데이터, 내부 구성이 들어 있을 때 중요합니다.
방법 2: TypeScript로 변환 직접 작성
빌드 스크립트나 코드 생성기의 일부로 프로그래밍 방식으로 타입을 생성해야 할 때는 로직을 직접 작성할 수 있습니다. 다음은 최소이지만 완전한 구현입니다.
function jsonToTypeScript(value: unknown, rootName = 'Root'): string {
const interfaces: string[] = [];
const seen = new Set<string>();
function cap(s: string) {
return s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Unknown';
}
function singular(name: string) {
if (/ies$/i.test(name)) return name.slice(0, -3) + 'y';
if (/[^s]s$/i.test(name)) return name.slice(0, -1);
return name + 'Item';
}
function getType(val: unknown, name: string): string {
if (val === null) return 'null';
if (typeof val === 'boolean') return 'boolean';
if (typeof val === 'number') return 'number';
if (typeof val === 'string') return 'string';
if (Array.isArray(val)) {
if (val.length === 0) return 'unknown[]';
const objs = val.filter(
(v): v is Record<string, unknown> =>
v !== null && typeof v === 'object' && !Array.isArray(v),
);
if (objs.length > 0) {
const itemName = cap(singular(name));
buildInterface(itemName, objs);
return `${itemName}[]`;
}
const types = [...new Set(val.map((v) => getType(v, name)))];
return types.length === 1 ? `${types[0]}[]` : `(${types.join(' | ')})[]`;
}
if (typeof val === 'object') {
const iName = cap(name);
buildInterface(iName, [val as Record<string, unknown>]);
return iName;
}
return 'unknown';
}
function buildInterface(name: string, objs: Record<string, unknown>[]): void {
if (seen.has(name)) return;
seen.add(name);
const allKeys = [...new Set(objs.flatMap((o) => Object.keys(o)))];
const props: string[] = [];
for (const key of allKeys) {
const present = objs.filter((o) => key in o);
const optional = present.length < objs.length ? '?' : '';
const propType = getType(present[0][key], cap(key));
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
props.push(` ${safeKey}${optional}: ${propType};`);
}
interfaces.push(`interface ${name} {\n${props.join('\n')}\n}`);
}
const rootType = getType(value, rootName);
const defs = interfaces.reverse().join('\n\n');
if (rootType !== cap(rootName)) {
return `type ${cap(rootName)} = ${rootType};\n\n${defs}`.trim();
}
return defs;
}Node.js 빌드 스크립트에서의 사용:
import * as fs from 'fs';
const json = JSON.parse(fs.readFileSync('api-response.json', 'utf8'));
const typescript = jsonToTypeScript(json, 'UserResponse');
fs.writeFileSync('types/user.ts', typescript);방법 3: quicktype (CLI 및 라이브러리)
quicktype은 JSON에서 TypeScript 타입을 생성하는 가장 기능이 풍부한 오픈소스 도구입니다. 단순한 생성기가 놓치는 엣지 케이스를 처리하고 여러 출력 형식을 지원합니다.
npm install -g quicktype
# 로컬 파일에서
quicktype api-response.json -o types/user.ts --lang typescript
# URL에서
quicktype https://api.example.com/users/42 -o types/user.ts --lang typescript
# 표준 입력에서
cat api-response.json | quicktype --lang typescriptquicktype은 io-ts나 zod 같은 라이브러리를 사용해 런타임 검증 함수도 생성합니다 —— 정적으로만 타입을 붙이는 게 아니라 들어오는 데이터를 검증해야 할 때 유용합니다.
까다로운 경우 다루기
nullable 필드
JSON API는 누락된 값에 종종 null을 반환합니다. null을 본 생성기는 타입으로 null을 만들지만 — 보통 원하는 건 string | null입니다.
// 생성됨 (첫 패스 —— 너무 좁음)
interface Root {
name: string;
middleName: null;
}
// 작성해야 할 형태
interface Root {
name: string;
middleName: string | null;
}생성 후에는 nullable 필드를 반드시 수동으로 검토하세요. 필드가 어떤 응답에선 문자열, 다른 응답에선 null인 여러 샘플이 있다면, 좋은 생성기는 자동으로 string | null을 추론합니다.
옵셔널 vs nullable
TypeScript는 field?: string(키가 없을 수 있음)와 field: string | null(키는 항상 존재하지만 값이 null일 수 있음)를 구분합니다.
interface Order {
id: number;
total: number;
discount?: number; // 일부 응답엔 이 키가 없음
note: string | null; // 키는 항상 있고 값이 null일 수 있음
}여러 JSON 샘플을 분석하는 생성기는 이 차이를 정확히 추론할 수 있습니다. 단일 샘플 생성기는 없는 키를 optional로 표시하지만 키 부재와 null 값을 구분하지 못합니다.
유효한 식별자가 아닌 키
일부 JSON 키에는 TypeScript 속성명으로 유효하지 않은 문자가 들어 있습니다:
// JSON
{ "content-type": "application/json", "x-request-id": "abc123" }
// 생성된 TypeScript —— 유효하지만 대괄호 표기 필요
interface Root {
"content-type": string;
"x-request-id": string;
}
// 대괄호 표기로 접근
obj["content-type"]API를 제어할 수 있다면 이 문제를 완전히 피하기 위해 camelCase 키를 선호하세요. 그렇지 않다면 따옴표가 있는 속성 구문은 유효한 TypeScript이며 올바르게 동작합니다.
혼합 타입 배열
// JSON
{ "values": [1, "two", true, null] }
// 생성됨
interface Root {
values: (number | string | boolean | null)[];
}이런 유니언 타입은 기술적으로 맞지만 보통 API 설계 문제의 신호입니다. 보게 되면 필드가 진짜 혼합 타입인지, 샘플 데이터가 오해를 일으키는 건지 확인하세요.
생성된 타입을 안전하게 사용하기
인터페이스 생성은 첫 단계일 뿐입니다. 인터페이스는 컴파일 타임 계약 —— 런타임에는 아무것도 검증하지 않습니다. response.json()은 any를 반환하고, TypeScript는 실제 검사 없이 인터페이스로 캐스팅하게 둡니다.
양쪽 모두 본인이 통제하는 내부 API에서는 보통 괜찮습니다. 외부 API나 사용자 제공 데이터의 경우에는 런타임에 검증하세요. 권장 방법은 Zod입니다.
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
active: z.boolean(),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
tags: z.array(z.string()),
orders: z.array(z.object({
id: z.number(),
total: z.number(),
status: z.string(),
})),
});
type User = z.infer<typeof UserSchema>; // 스키마에서 파생된 TypeScript 타입
const raw = await response.json();
const user = UserSchema.parse(raw); // 형태가 맞지 않으면 throwZod의 접근 방식은 두 가지를 모두 잡습니다: 스키마가 TypeScript 타입과 런타임 검증의 단일 진실 원천입니다. API가 호환되지 않게 변경되면 데이터가 시스템에 들어오는 지점에서 바로 알 수 있습니다.
인터페이스 너머: 검증기와 타입화된 클라이언트
생성된 인터페이스는 컴파일 타임 계약입니다. 한 단계 위는 단일 소스에서 타입과 런타임 체크를 동시에 제공하는 라이브러리를 사용하는 것입니다:
- Zod 와 io-ts —— 스키마를 정의하고 거기서 TypeScript 타입을 추론하며 경계에서 들어오는 데이터를 검증합니다. API가 바뀌면 빌드(와 오류 로그)가 즉시 알려줍니다.
- tRPC 와 타입화된
fetch래퍼 (예:openapi-fetch) —— 서버의 프로시저나 OpenAPI 정의가 진실 원천이고, 클라이언트는 일치하는 타입을 자동으로 가져옵니다. 중간에 JSON→TS 단계가 없습니다. - ts-pattern 과 판별 유니언 —— 데이터에 태그 필드(예:
{ kind: "circle" | "square", ... })가 있으면, 망라적 패턴 매칭으로 컴파일 타임에 누락된 케이스를 잡아냅니다.
타입을 API와 동기화 유지하기
생성된 타입에는 유통기한이 있습니다. API는 변합니다 —— 필드명이 바뀌거나, 새 필수 필드가 생기거나, 문자열 필드가 nullable이 되거나. 인터페이스는 조용히 낡아갑니다.
샘플 응답이 아닌 OpenAPI 스펙에서 생성
API에 OpenAPI(Swagger) 정의가 있다면 openapi-typescript가 스펙에서 직접 타입을 생성합니다:
npm install -g openapi-typescript
openapi-typescript https://api.example.com/openapi.json -o types/api.ts이는 샘플 응답에서 생성하는 것보다 훨씬 신뢰할 수 있습니다. 스펙은 옵셔널, nullable 변형, 열거 값을 포함한 모든 필드를 문서화하기 때문입니다.
CI에서 타입 재생성
CI 파이프라인에 스펙이나 알려진 정상 샘플에서 타입을 재생성한 뒤 tsc --noEmit를 실행해 깨짐을 확인하는 단계를 추가하세요. API가 바뀌면 호환되지 않는 변경이 운영에 도달하기 전에 빌드가 실패합니다.
자주 묻는 질문
JSON을 TypeScript 인터페이스로 어떻게 변환하나요?
즉시 결과를 원한다면 fixjson의 JSON-to-TypeScript 도 구 같은 온라인 변환기에 붙여넣고, 빌드 단계에서는 quicktype을 실행하거나, 장기 유지 코드에는 openapi-typescript로 OpenAPI 스펙에서 생성하세요.
JSON 타입은 TypeScript 타입에 어떻게 매핑되나요?
문자열 → string, 모든 숫자 → number (TypeScript에 정수 타입 없음), boolean → boolean, 객체 → 이름 있는 인터페이스, 배열은 요소 타입을 추론. 위의 매핑 표와 JSON의 여섯 가지 데이터 타입을 참고하세요.
nullable이나 옵셔널 필드는 어떻게 다루나요?
키가 없을 수 있을 때는 field?: T를, 키는 항상 있지만 값이 null일 수 있을 때는 field: T | null을 사용하세요. 여러 샘플을 분석하는 생성기는 이를 추론하지만 단일 샘플 도구는 수동 검토가 필요합니다.
TypeScript 인터페이스가 런타임에 JSON을 검증하나요?
아니요 —— 인터페이스는 컴파일 타임에 사라집니다. 외부 데이터에 대한 런타임 안전이 필요하면 Zod로 검증하거나 JSON Schema로 형태를 기술하세요.
결론
JSON을 TypeScript 인터페이스로 변환하는 일은 모든 TypeScript 개발자가 정기적으로 합니다. 맞는 접근법은 상황에 따라 다릅니다: 빠른 일회성 연동에는 온라인 변환기로 1분 안에 시작할 수 있습니다. 빌드 파이프라인에는 quicktype이나 커스텀 스크립트가 과정을 자동화합니다. 장기 운영 코드에는 OpenAPI 스펙에서 타입을 생성하고 Zod로 런타임 검증하는 조합이 가장 강력한 보장을 제공합니다.
어떤 방법을 쓰든 생성된 인터페이스는 출발점입니다 —— nullable 필드를 검토하고, 자명하지 않은 속성엔 주석을 추가하고, 비즈니스 규칙이 JSON 샘플이 의미하는 것보다 엄격한 곳에서는 제약을 조이세요. 데이터를 정확히 반영하는 타입은 코드베이스 전체의 유지보수를 더 쉽게 만듭니다.
변환을 시도하기 전에 JSON 자체에 구문 오류가 있다면 먼저 수정하세요. TypeScript 생성기는 다른 모든 JSON 파서처럼 구문상 유효한 입력이 필요합니다.