OpenAPI 스펙으로 타입을 생성하고 AI가 API 계약을 지키게 하기
openapi-typescript 타입 생성, openapi-fetch 클라이언트, CI 계약 검사
핵심 요약
- OpenAPI 스펙을 API 계약의 단일 기준으로 두고 TypeScript 타입을 생성한다.
- openapi-typescript는 OpenAPI 3.0·3.1을 지원하며 런타임 의존성 없는 타입을 만든다.
- 규칙 파일로 AI가 생성 타입과 공용 클라이언트만 쓰게 하고 스펙에 없는 필드는 변경 제안으로 처리하게 한다.
- CI에서 타입을 재생성해 커밋된 파일과 비교하고 tsc로 깨진 호출을 찾는다.
AI 코딩 도구에 "주문 목록 API를 호출하는 화면 만들어 줘"라고 하면, 실제 응답 형식을 모르는 상태에서 그럴듯한 필드 이름을 지어내는 경우가 많다. orderId인지 id인지, 금액이 숫자인지 문자열인지 틀리면 런타임에서야 오류가 드러난다. 해결책은 API 계약을 OpenAPI 스펙 하나로 정하고, 거기서 TypeScript 타입을 생성해 AI가 그 타입을 쓰게 만드는 것이다. 이 글은 openapi-typescript 문서를 기준으로 구성 방법을 정리한다. 백엔드 API 설계는 FastAPI 비동기 API 설계 글에서 다뤘다.
계약을 스펙 하나로 모으면 달라지는 점
| 상황 | 스펙이 없을 때 | 스펙에서 타입을 생성할 때 |
|---|---|---|
| AI가 API 호출 코드 작성 | 필드 이름·타입을 추측 | 생성된 타입을 import해 컴파일 단계에서 검사 |
| 백엔드 응답 변경 | 프론트엔드 런타임 오류로 발견 | 타입 재생성 시 컴파일 오류로 발견 |
| 리뷰 | 응답 형식을 문서와 대조 | 타입 검사 통과 여부로 확인 |
1단계: openapi-typescript로 타입 생성
npm i -D openapi-typescript typescript
# 로컬 스펙 파일에서 생성
npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts
# 원격 스펙에서 생성
npx openapi-typescript https://api.example.com/openapi.yaml -o ./src/api/schema.d.ts문서에 따르면 OpenAPI 3.0과 3.1을 지원하며(판별자 같은 고급 기능 포함), 생성 결과는 런타임 의존성이 없는 타입 선언이다. 2.x 스펙은 이전 메이저 버전(5.x 이하)이 지원한다. 문서는 타입 안전성을 위해 tsconfig.json에 noUncheckedIndexedAccess를 켜는 것을 강하게 권한다.
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"noUncheckedIndexedAccess": true
}
}2단계: 생성된 타입으로 API 호출하기
같은 프로젝트의 openapi-fetch를 쓰면 경로 문자열만으로 요청 매개변수와 응답 타입이 자동으로 맞춰진다.
// src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from './schema'
export const api = createClient<paths>({ baseUrl: 'https://api.example.com' })
// 사용 예: 경로와 매개변수가 스펙과 다르면 컴파일 오류
const { data, error } = await api.GET('/orders/{orderId}', {
params: { path: { orderId: 'ORD-20261003' } },
})
if (error) {
// 스펙에 정의된 오류 응답 타입
} else {
console.log(data.status) // 스펙에 없는 필드를 쓰면 타입 오류
}직접 fetch를 쓰는 기존 코드가 있다면 paths['/orders/{orderId}']['get']['responses']['200']['content']['application/json']처럼 생성 타입을 꺼내 응답에 지정하는 방식으로 점진적으로 옮길 수 있다.
3단계: AI 도구에 계약을 알려 주기
- 규칙 파일(Cursor Rules, CLAUDE.md, copilot-instructions.md)에 "API 호출은
src/api/client.ts의api만 사용, 응답 타입은schema.d.ts에서 가져온다"를 적는다. - "스펙에 없는 필드가 필요하면 코드를 작성하지 말고 스펙 변경을 제안하라"는 지시를 함께 둔다.
- API 작업을 요청할 때 관련 스펙 경로(예:
/orders/{orderId})를 요청 문장에 넣는다. - 생성 타입 파일은 사람이 수정하지 않는다는 주석과 함께 저장소에 커밋하거나, 빌드 단계에서 생성한다.
4단계: CI에서 스펙과 타입이 어긋나지 않게 하기
# 스펙에서 타입을 다시 생성해 커밋된 파일과 다르면 실패
npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts
git diff --exit-code src/api/schema.d.ts
# 생성 타입 기준으로 전체 타입 검사
npx tsc --noEmit백엔드가 스펙을 바꾸면 프론트엔드 CI가 타입 재생성 단계에서 차이를 감지하고, 깨진 호출 코드를 타입 검사가 찾아낸다. 백엔드 쪽에서는 스펙 변경을 PR로 올리고 프론트엔드 담당자를 리뷰어로 지정하면 계약 변경이 조용히 들어가는 일을 막을 수 있다.
운영하면서 정할 규칙
| 결정 | 권장 |
|---|---|
| 스펙의 원본 위치 | 백엔드 저장소 한 곳(또는 별도 계약 저장소) |
| 스펙 작성 방식 | 코드에서 자동 생성하든 직접 작성하든 한 가지로 통일 |
| 호환되지 않는 변경 | 필드 삭제·이름 변경은 새 버전 경로나 단계적 폐기 절차로 |
| 생성 타입 갱신 주기 | 스펙 변경 PR마다 자동 갱신 |
타입이 AI 코딩 품질에 주는 효과는 TypeScript와 AI 코딩 품질 글에서, 생성 코드의 검증 절차는 AI 생성 코드 테스트 워크플로우 글에서 다뤘다.
초보자가 자주 실수하는 포인트
- AI가 만든 응답 타입을 그대로 받아들여 스펙과 다른 필드를 쓰는 경우
- 생성된 타입 파일을 손으로 고쳐 다음 생성 때 변경이 사라지는 경우
- noUncheckedIndexedAccess 없이 배열·객체 접근의 undefined 가능성을 놓치는 경우
- 백엔드 스펙 변경을 프론트엔드에 알리지 않고 배포하는 경우
체크리스트
- API 계약이 하나의 OpenAPI 스펙 파일로 관리되는가
- 타입 생성 명령이 스크립트로 등록돼 있는가
- 규칙 파일에 공용 클라이언트 사용 규칙이 있는가
- CI가 타입 재생성 diff와 tsc 검사를 수행하는가
- 호환되지 않는 스펙 변경 절차가 정해져 있는가
자주 묻는 질문
Swagger 2.0 스펙도 쓸 수 있나요?
문서에 따르면 현재 버전은 OpenAPI 3.0·3.1을 지원하고, 2.x는 5.x 이하 버전이 지원한다. 가능하면 스펙을 3.x로 변환한다.
생성 타입 파일을 저장소에 커밋해야 하나요?
커밋하면 리뷰에서 변경을 볼 수 있고, 빌드 시 생성하면 파일이 어긋날 걱정이 없다. 팀 상황에 맞게 하나로 정하고 CI에서 일관성을 검사한다.
백엔드가 FastAPI면 스펙을 따로 써야 하나요?
FastAPI처럼 코드에서 OpenAPI 스펙을 생성하는 프레임워크라면 그 결과 파일을 계약 원본으로 쓸 수 있다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.