튜토리얼
OpenAPI로 JSON API 문서, TypeScript 타입, API Client를 자동 생성하려면? Schema에서 코드 생성까지
손문서, 손타입, 손 fetch가 제각각 어긋나는 비용이 엔드포인트보다 크다. OpenAPI Schema를 계약으로 두고 문서, 타입, Client를 같은 JSON에서 키운다.
GET /invoices에서 비싼 것은 구현이 아니라 옆에 따로 상하는 세 가지다. 사람용 API 문서, 컴파일러용 TypeScript 타입, 런타임 fetch 포장. 지금도 Wiki에 예시 JSON을 붙이고 types.ts에 같은 필드를 다시 쓰며 api.ts에서 URL을 이어 붙이는 팀이 있다. OpenAPI Schema의 가치는 이를 기계가 읽는 하나의 계약으로 접는 것이다. 당신은 openapi.json(또는 YAML)을 유지한다. 문서 사이트, 타입 파일, Client는 거기서 생성된다. 필드를 한 번 바꾸면 세 곳이 같이 움직인다. 도구 인자는 MCP와 JSON Schema, 모델 최종 답은 구조화 출력. 이 글은 HTTP JSON API만 다룬다. Schema에서 문서, 타입, Client. 규격은 OpenAPI 3.1. 기본 생성기는 openapi-typescript.
Schema 하나, 산물 셋: 문서, 타입, Client
목표를 먼저 고정한다. OpenAPI 문서는 또 다른 Markdown이 아니라 같은 spec의 렌더다. Scalar, Swagger UI, Redoc는 읽기 도구일 뿐이다. TypeScript 타입은 두 번째 손 interface가 아니라 paths와 components.schemas의 투영이다. API Client는 「axios를 한 겹 더」가 아니라 operationId나 경로에서 나온, 요청·응답 타입을 단 함수다. 셋 중 둘이 손이면 Schema는 벽 포스터다.
도구는 나중에 고른다. 계약이 먼저다. FastAPI, Nest, Spring, Go 주석에서 spec을 내보내도 되고, openapi.json을 먼저 쓰고 구현을 맞춰도 된다. 금지하는 것은 Client를 먼저 쓰고 호출자의 「느낌」으로 문서를 채우는 것이다. 단일 진실이 없고 CI는 누가 맞는지 판단할 수 없다. Agent가 REST를 도구로 쓰면 이 OpenAPI를 읽어야 한다. 두 번째 MCP inputSchema를 베끼지 마라. 두 계약은 바로 drift한다. 층은 위의 MCP 글을 보라.
OpenAPI 3.1은 components.schemas를 JSON Schema 2020-12에 더 붙인다. nullable은 type: ["string", "null"]이지 3.0의 nullable: true가 아니다. 「문서에는 선택, 타입에는 필수」가 줄어든다. JSONVue에서 보는 객체가 생성기가 먹는 객체여야 한다. 보낼 때 방언으로 바꾸지 마라.
| 산물 | 어디서 오나 | 손으로 쓰지 말 것 |
|---|---|---|
| JSON API 문서 | 같은 OpenAPI JSON을 Scalar / Swagger UI로 렌더 | Wiki의 예시 JSON과 지난 스크린샷 |
| TypeScript 타입 | openapi-typescript가 paths / components를 냄 | spec과 나란히 가는 interface Invoice |
| API Client / hooks | openapi-fetch, Orval, hey-api가 호출 함수를 냄 | 손 URL과 as Invoice |
먼저 OpenAPI JSON을 맞게 쓴다: paths, components, operationId
생성기의 상한은 spec 품질이다. 최소는 openapi 버전, info, paths, 재사용 모델을 components.schemas에. 각 조작에 안정된 operationId(listInvoices, createInvoice)를 준다. Client가 함수 이름으로 쓴다. 경로가 바뀌어도 id를 따라 흔들지 마라. 요청과 성공 응답에 application/json을 선언하고 컴포넌트로 $ref하라. 각 path에 필드표를 복제하지 마라.
오류 응답도 계약에 넣는다. 400 / 401 / 404 / 422에 JSON Schema를 주면 Client가 unknown이 아닌 읽을 수 있는 유니온을 만든다. 쿼리와 경로 파라미터는 parameters에 쓰고 body에 숨긴 채 문서로 구두 약속하지 마라. Bearer, OIDC는 securitySchemes에 쓴다. 생성된 Client가 토큰 위치를 안다. 인증 프로토콜 자체는 OAuth 2.1. REST에서도 토큰은 헤더이지 JSON 봉투가 아니다.
아래는 최소 청구 API다. 제품 spec은 아니지만 문서, 타입, Client 세 줄을 동시에 돌리기에 충분하다. Invoice는 한 번만 나오고 list와 create가 참조한다. 먼저 로컬에서 JSON 포맷으로 parse를 확인하고 Schema로 $ref가 끊겼는지 본다.
{
"openapi": "3.1.1",
"info": {
"title": "Invoices API",
"version": "2026.09.18"
},
"paths": {
"/invoices": {
"get": {
"operationId": "listInvoices",
"parameters": [
{
"name": "status",
"in": "query",
"schema": {
"type": "string",
"enum": ["draft", "paid", "void"]
}
}
],
"responses": {
"200": {
"description": "Invoice list",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["items"],
"properties": {
"items": {
"type": "array",
"items": { "$ref": "#/components/schemas/Invoice" }
}
}
}
}
}
}
}
},
"post": {
"operationId": "createInvoice",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvoiceDraft" }
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Invoice" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"InvoiceDraft": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": { "type": "string" },
"amount": { "type": "integer", "minimum": 1 },
"note": { "type": ["string", "null"] }
}
},
"Invoice": {
"allOf": [
{ "$ref": "#/components/schemas/InvoiceDraft" },
{
"type": "object",
"required": ["id", "status"],
"properties": {
"id": { "type": "string" },
"status": {
"type": "string",
"enum": ["draft", "paid", "void"]
}
}
}
]
}
}
}
}
사람이 읽는 JSON API 문서를 생성한다
올바른 문서 사이트는 빌드 때 openapi.json을 정적 폴더에 복사하고 옆에 리더를 단다. Scalar, Swagger UI, Redoc는 같은 파일을 읽는다. 「문서」를 또 한 더미 Markdown으로 생각하고 CI에서 spec과 두 번 대조하지 마라. 사람이 문서를 고치고 기계가 spec을 고치면 다음 날 갈라진다.
리더가 보여줄 수 있는 것은 spec에 적힌 것뿐이다. example이 없으면 복사할 예가 없고, 오류 응답이 없으면 실패 설명이 없으며, description이 비면 경로만 남는다. 문서를 만들기 전에 계약을 채워라. 미리보기는 브라우저에서 리더를 열면 된다. spec 자체는 JSONVue로 구조를 보는 편이 거대한 YAML에서 끊긴 $ref를 찾는 것보다 빠르다.
문서 URL을 info.contact나 내부 포털에 적어도 된다. 다만 제품용 두 번째 필드표를 유지하지 마라. 제품이 보고 싶은 열거와 필수는 이미 Schema의 enum / required에 있다. 필드표 두 장은 drift의 정석 입구다.
TypeScript 타입 생성: openapi-typescript
타입 생성 기본은 openapi-typescript면 충분하다. 런타임 제로, OpenAPI 3.0과 3.1, paths와 components를 낸다. 명령은 npx openapi-typescript openapi.json -o src/api/schema.d.ts. 원격 spec도 된다. 프로덕션 CI는 저장소 파일을 고정하고 변하는 URL을 치게 하지 마라.
import type { paths, components } from "./schema";
type Invoice = components["schemas"]["Invoice"];
type ListOk =
paths["/invoices"]["get"]["responses"][200]["content"]["application/json"];
export type { Invoice, ListOk };
타입은 경로로 인덱싱한다. 새 interface를 선언하지 마라. paths["/invoices"]["get"]["responses"][200]["content"]["application/json"]이 목록 성공 몸이다. 모델을 재사용하면 components["schemas"]["Invoice"]. 경로가 바뀌면 세 개의 Invoice를 찾는 대신 호출부에서 컴파일러가 실패한다.
타입은 런타임 검사가 아니다. 생성된 .d.ts는 브라우저에 없다. 선로는 여전히 빠진 필드 JSON을 보낼 수 있다. 컴파일 통과는 계약대로 Client를 썼다는 뜻일 뿐이다. 런타임 문이 필요하면 Orval/Kubb로 Zod를 내거나 핵심 응답을 Schema 검사하라. 「TypeScript가 초록」을 유일한 QA로 두지 마라.
API Client 생성: openapi-fetch, Orval, hey-api
타입만 필요하면 openapi-fetch를 붙인다. createClient<paths>({ baseUrl }) 다음 client.GET("/invoices", { params: { query } }). 본문, 쿼리, 경로 파라미터가 타입으로 막힌다. createInvoice body를 list에 넣을 수 없다. 이것이 2026 프론트의 얇은 기본이다. 파일은 적고 fetch는 내가 쥐며 생성물 창고가 없다.
import createClient from "openapi-fetch";
import type { paths } from "./schema";
const client = createClient({
baseUrl: "https://api.example.com",
headers: { Authorization: "Bearer …" }
});
const { data, error } = await client.GET("/invoices", {
params: { query: { status: "paid" } }
});
const created = await client.POST("/invoices", {
body: { customerId: "cus_1", amount: 1999, note: null }
});
React Query hooks, Zod, MSW mock이 필요하면 Orval을 올린다. 같은 spec에서 함수와 훅이 자란다. 이미 Query를 데이터 층으로 쓰는 앱에 맞다. hey-api(@hey-api/openapi-ts)는 SDK 함수와 인터셉터다. operationId를 메서드 이름으로 쓰되 수천 파일은 싫은 팀용. OpenAPI Generator의 typescript-axios는 아직 동작하지만 출력이 무겁다. 새 프로젝트의 첫 선택이 될 필요는 없다.
어떤 파이프라인이든 생성 파일을 손으로 고치지 마라. 동작을 바꾸려면 spec, 생성기 설정, 또는 손 래퍼(타임아웃, 재시도, 테넌트 헤더)다. 생성물을 손보면 계약을 버리는 것이다. 토큰은 Authorization 헤더에 두고 JSON body에 넣지 마라.
| 도구 | 얻는 것 | 고를 때 |
|---|---|---|
| openapi-typescript + openapi-fetch | 타입 + 얇은 fetch Client, 런타임에 가깝게 제로 | 기본 출발. 캐시를 내가 쥘 때 |
| Orval | 타입 + 함수 + Query hooks + 선택 Zod/MSW | 이미 TanStack Query를 쓰고 mock이 spec을 따라가야 할 때 |
| hey-api / Kubb | SDK 메서드 이름, 또는 플러그인 다산물 | 인터셉터 플랫폼, 또는 operation당 파일 하나 |
CI에서 drift를 막고 JSONVue로 계약을 본다
계약이 바뀌면 생성물을 다시 돌린다. CI에서는 spec 검사(Spectral 또는 Redocly) → 생성기 실행 → 생성 디렉터리에 git diff --exit-code. diff가 있으면 실패시켜 새 타입을 커밋하게 한다. 노트북에서 codegen 한 번 하고 잊지 마라. 백엔드가 필드를 바꿨는데 프론트 타입이 지난주인 것이 이 사고의 표준형이다.
npx --yes openapi-typescript openapi.json -o src/api/schema.d.ts
npx --yes @redocly/cli lint openapi.json
git diff --exit-code -- src/api/schema.d.ts
연동에서 적어도 JSON 세 장을 만난다. OpenAPI spec 자체, 어떤 200 body, 어떤 422 오류 몸, 그리고 생성기에 넣을 얇은 픽스처. 역할이 다르다. 「범용 검증」 한 파일에 섞지 마라. 실패 샘플을 하나씩 남겨라. 끊긴 $ref, 빠진 operationId, 응답 content에 application/json이 없음, 열거만 바뀐 두 버전 spec.
브라우저에서 끝낼 수 있다:JSON 포맷으로 spec이 parse되는지 보고,JSON Schema 검증으로 components.schemas와 실제 body를 확인하고,JSON Diff로 두 OpenAPI 버전이나 모델 arguments 대 하류 body를 비교한다. 데이터는 이 기기를 떠나지 않는다. 이어서:MCP와 JSON Schema, 구조화 출력, 같은 필드표로 도구 껍질을 생성하는 것.
자주 묻는 질문 FAQ
spec은 YAML인가 JSON인가?
생성기는 둘 다 먹는다. 저장소에서는 YAML이 사람 diff에 더 낫다. 브라우저와 JSONVue에는 JSON을 보내라. CI에서는 소스 파일을 하나로 고정하고 다른 쪽은 빌드 산물만 두어 두 소스가 서로를 고치지 않게 한다.
타입만 생성하고 Client는 안 내도 되나?
된다. 오히려 그래야 할 때가 많다. openapi-typescript는 .d.ts만 낸다. 기존 axios 포장은 이 타입을 조금씩 먹기 시작하면 된다. 경로와 body가 타입으로 막힌 뒤에 openapi-fetch나 Orval로 바꿔라. 첫날 三千 파일을 내는 것보다 통제하기 쉽다.
새 프로젝트는 OpenAPI 3.0인가 3.1인가?
새 계약은 3.1. JSON Schema 2020-12에 맞추고 nullable과 const 방언을 한 층 줄인다. 기존 3.0을 억지로 올리지 마라. 생성기(openapi-typescript 7.x, Orval 8)가 3.1을 명시한 뒤에 옮겨라. Swagger 2.0은 먼저 3.x로 올린 다음 Client를 이야기하라.
OpenAPI와 MCP inputSchema를 한 장으로 쓸 수 있나?
업무 필드는 canonical JSON Schema 한 장을 공유한 뒤 OpenAPI requestBody와 MCP inputSchema를 따로 생성할 수 있다. OpenAPI 파일 전체를 MCP Client의 도구 목록으로 넘기지 마라. 전송, 인증, 메서드 이름은 같은 층이 아니다. 필드표 한 장, 껍질 두 장이 복사보다 drift가 적다.
요약과 다음 단계
OpenAPI 코드 생성은 한 문장이다. 기계가 읽는 Schema가 사람이 읽는 JSON API 문서, 컴파일 타임 TypeScript 타입, 타입이 있는 Client를 같이 키운다. 그중 하나를 손으로 쓰면 계약은 계약이 아니다.
올리는 순서: openapi.json이 parse되고 operationId가 있으며 오류 응답이 갖춰진다. 그다음 문서 리더. 그다음 타입. 마지막에 Client. spec, 성공 body, 실패 body를 JSONVue로 이 기기에 남겨라. 도구 인자는 MCP와 JSON Schema. 모델 답은 구조화 출력.