OpenAPI 스펙 한 파일로 TypeScript API drift를 없애는 법 — Codex CLI + AGENTS.md
백엔드 개발자가 응답 필드를 하나 추가했는데 프론트엔드는 여전히 any로 받고 있었다는 경험, 한 번쯤은 있을 겁니다. 저도 솔직히 꽤 오래 이 패턴으로 버텼는데, "어차피 런타임에서 터지면 그때 고치면 되지"라는 안일함이 결국 배포 당일 밤 핫픽스로 돌아왔습니다. OpenAPI 스펙을 단일 소스 오브 트루스로 삼고, 타입 코드를 아예 자동 생성하면 이 문제가 구조적으로 사라집니다. 스펙이 바뀌면 타입이 바뀌고, 타입이 바뀌면 컴파일러가 먼저 알려주니까요.
여기에 Codex CLI와 AGENTS.md를 얹으면 한 가지가 더 해결됩니다. AI 코딩 에이전트가 "편의상" 생성된 파일을 직접 수정하는 상황을 원천 차단할 수 있습니다. AGENTS.md에 재생성 규칙을 명시해두면 Codex CLI는 세션이 바뀌어도, 팀원이 바뀌어도 그 규칙을 따릅니다. 이 글에서는 이 두 가지를 엮는 구체적인 방법을 살펴봅니다.
API drift가 생기는 구조적 이유
문제는 "타입을 두 번 쓴다"는 것
백엔드에 UserResponse 타입이 있고, 프론트엔드에도 UserResponse 인터페이스가 있습니다. 처음엔 동일하지만 시간이 지나면 두 코드베이스는 독립적으로 진화합니다. 백엔드가 필드를 추가해도 프론트엔드 타입에는 반영되지 않습니다. 이걸 API drift라고 부르는데, 타입이 두 군데 존재하는 한 피할 수 없는 구조적 문제입니다.
먼저 문제 구조부터 보겠습니다.
이 흐름을 뒤집으면 개선된 구조가 됩니다.
해결책은 타입의 소유권을 하나로 통일하는 것입니다. OpenAPI 스펙이 그 역할을 합니다. 스펙을 먼저 정의해 두면(spec-first) 백엔드와 프론트엔드가 같은 계약을 참조하고, 중간에 수동 동기화 단계가 없으니 drift가 생길 틈이 없습니다. FastAPI처럼 코드에서 스펙을 뽑아내는(code-first) 환경이라면 스펙이 결과물이라는 점만 다를 뿐, 프론트엔드 입장에서는 동일하게 스펙을 입력으로 사용합니다.
2026년의 코드젠 생태계
TypeScript 코드젠 툴은 하나로 좁혀지지 않았습니다. 각자 지향점이 다르고, 프로젝트 특성에 따라 선택이 갈립니다. 그중 최근 몇 년간 가장 널리 채택되고 있는 옵션이 @hey-api/openapi-ts입니다. openapi-typescript-codegen의 후계자 격으로, 플러그인 아키텍처를 갖춰서 TanStack Query, Axios, Fetch 등 다양한 클라이언트를 붙일 수 있습니다.
| 도구 | 특징 | 적합 사용처 |
|---|---|---|
| @hey-api/openapi-ts | 플러그인 아키텍처, TanStack Query 지원 | 범용 TypeScript 프로젝트 |
| Orval | 훅 자동 생성, 내장 목(mock) 생성 | React/테스트 중심 프로젝트 |
| openapi-typescript | 타입만 생성, 가볍고 유연 | 직접 fetch 코드를 선호할 때 |
| Kubb | 멀티 플러그인, Zod 스키마 생성 가능 | 복잡한 모노레포 환경 |
| OpenAPI Generator | 다중 언어 지원 | 멀티 스택 엔터프라이즈 환경 |
이 글에서는 @hey-api/openapi-ts를 예시로 진행하지만, 아래 설명하는 AGENTS.md 패턴은 어느 툴을 골라도 동일하게 적용됩니다.
기본 셋업 — 코드젠부터 AGENTS.md까지
1단계: 코드젠 설치 및 스크립트 등록
npm install -D @hey-api/openapi-ts버전을 고정하는 게 중요합니다. 고정하지 않으면 환경마다 생성 결과물이 달라져서 diff가 지저분해집니다.
{
"scripts": {
"codegen": "openapi-ts"
},
"devDependencies": {
"@hey-api/openapi-ts": "0.52.3"
}
}@hey-api/openapi-ts는 비교적 이른 시점부터 CLI 플래그 대신 설정 파일 방식을 권장해 왔습니다(개념적 예시이므로 실제 사용 시 설치한 버전의 문서를 확인하세요). 프로젝트 루트에 openapi-ts.config.ts를 두는 방식이 유지보수하기도 더 편합니다.
// openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: './openapi.yaml',
output: './src/generated',
plugins: ['@hey-api/client-fetch'],
});2단계: 처음 실행해서 구조 확인
npm run codegensrc/generated/ 아래에 이런 파일들이 생깁니다(생성 파일 목록과 이름은 버전·플러그인 조합에 따라 달라질 수 있으니, 처음 실행 후 실제 결과물을 기준으로 삼는 게 좋습니다).
src/generated/
├── schemas.gen.ts # OpenAPI 스키마 → TypeScript 타입
├── sdk.gen.ts # API 호출 함수
├── types.gen.ts # 요청/응답 타입
└── client.gen.ts # fetch 클라이언트 인스턴스파일명에 .gen.ts가 붙는 건 "이 파일은 생성된 것"이라는 시각적 신호입니다. 저도 처음엔 이걸 직접 고치려다가 다음 codegen에서 덮어씌워져서 황당했던 적이 있습니다.
3단계: 실제 코드에서 사용
import { getUser } from './generated/sdk.gen';
const { data, error } = await getUser({ path: { userId: 42 } });
if (error) throw error;
console.log(data.email); // email: string 으로 추론됨백엔드가 email 필드를 emailAddress로 이름을 바꾸면, 다음 codegen 실행 후 이 줄에서 컴파일 오류가 납니다. 런타임이 아니라 빌드 단계에서요.
4단계: AGENTS.md로 규칙 고정
이제 핵심입니다. Codex CLI는 프로젝트를 열 때 AGENTS.md를 자동으로 읽습니다. Claude Code의 CLAUDE.md에 해당하는 역할입니다. 여기에 생성 파일 규칙을 못 박아두면 에이전트가 "일단 이 파일만 살짝 고칠게요"를 할 수 없게 됩니다.
# AGENTS.md
## Generated Code Rules
`src/generated/` 디렉터리는 OpenAPI 스펙에서 자동 생성된 코드입니다.
이 디렉터리의 파일은 절대 직접 수정하지 않습니다.
API 타입이나 클라이언트 코드를 변경해야 할 경우:
1. `openapi.yaml`을 먼저 수정한다
2. `npm run codegen`을 실행하여 재생성한다
3. 생성된 파일 전체를 커밋에 포함한다
## Codegen Command
```bash
npm run codegen
# 설정은 openapi-ts.config.ts 참조
```
## Wrapper Pattern
생성된 클라이언트에 커스텀 로직이 필요하면 `src/api/` 아래에 래퍼 레이어를 작성한다.
`src/generated/`를 직접 수정하는 방식은 허용하지 않는다.이걸 저장소에 커밋해두면 Codex CLI는 세션이 시작될 때마다 이 규칙을 읽고 세션 전체에서 준수합니다.
5단계: Codex CLI로 재생성 위임
스펙이 바뀌었을 때 Codex CLI에게 이렇게 시킬 수 있습니다.
codex "openapi.yaml의 변경사항을 반영해 src/generated를 재생성해줘"AGENTS.md가 있으면 에이전트는 파일을 직접 수정하지 않고 npm run codegen을 실행한 다음, 생성된 결과물을 확인하는 방식으로 동작합니다.
활용 패턴 — 모노레포·백엔드·래퍼 레이어
모노레포에서 계층적으로 배치하기
여러 패키지가 각각 다른 백엔드 스펙을 바라보는 모노레포라면 AGENTS.md를 계층적으로 배치할 수 있습니다. Codex CLI는 하위 디렉터리의 AGENTS.md도 컨텍스트에 반영하도록 설계되어 있는데, 정확한 탐색 규칙과 우선순위는 버전에 따라 달라질 수 있으니 배포 전 공식 저장소의 AGENTS.md 문서에서 최신 동작을 확인해 두는 것을 권장합니다.
루트 AGENTS.md에 전역 규칙(생성 디렉터리 직접 수정 금지 등)을 쓰고, 각 패키지의 AGENTS.md에는 해당 패키지의 codegen 명령을 명시합니다.
FastAPI 백엔드와 연결하기
FastAPI는 /openapi.json을 자동으로 노출합니다. 이걸 직접 입력으로 쓸 수 있습니다.
// openapi-ts.config.ts (개발용)
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: 'http://localhost:8000/openapi.json',
output: './src/generated',
plugins: ['@hey-api/client-fetch'],
});CI/CD에서는 스펙 파일을 아티팩트로 내보내서 정적으로 참조하는 편이 안정적입니다. GitHub Actions 예시입니다.
# .github/workflows/codegen.yml
name: Codegen on spec change
on:
push:
paths:
- 'openapi.yaml'
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run codegen
- uses: peter-evans/create-pull-request@v6
with:
title: 'chore: regenerate API client from updated spec'
branch: codegen/auto-update스펙이 바뀌면 자동으로 PR이 열리고, 리뷰를 거쳐 병합됩니다. 스펙 변경이 "슬쩍 올라가는 것"이 아니라 리뷰 대상 계약 변경으로 취급되는 겁니다.
커스텀 로직은 래퍼 패턴으로
생성된 코드를 직접 건드릴 수 없다는 게 처음엔 답답하게 느껴질 수 있습니다. 하지만 래퍼 레이어를 분리하면 오히려 관심사가 명확해집니다.
@hey-api/openapi-ts의 fetch 클라이언트는 개별 호출마다 헤더를 파라미터로 넘기는 방식보다는, 클라이언트 인스턴스에 설정을 심어두거나 인터셉터를 다는 방식이 표준입니다. 아래는 그 구조를 반영한 개념적 예시입니다.
// src/api/client.ts — 부트스트랩 시 1회 설정
import { client } from '../generated/client.gen';
import { authStore } from '../stores/auth';
client.setConfig({
baseUrl: import.meta.env.VITE_API_BASE_URL,
});
client.interceptors.request.use((request) => {
const token = authStore.getToken();
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
return request;
});// src/api/users.ts — 도메인 래퍼
import { getCurrentUser as getCurrentUserSdk } from '../generated/sdk.gen';
export async function getCurrentUser() {
const { data, error } = await getCurrentUserSdk();
if (error) {
throw new Error(`Failed to load current user: ${error.message ?? 'unknown'}`);
}
return data;
}src/generated/는 손대지 않는 영역, src/api/는 우리가 제어하는 영역으로 명확하게 나뉩니다. 이 경계를 AGENTS.md에 써두면 AI 에이전트도 이 선을 지킵니다.
트레이드오프 — 솔직하게
이 워크플로우가 모든 상황에 맞는 건 아닙니다. 도입 전에 고려해볼 것들입니다.
잘 동작하는 상황:
- 백엔드-프론트엔드 팀이 분리되어 있고 스펙이 계약서 역할을 해야 할 때
- API surface가 넓고 타입을 수동으로 유지하는 비용이 클 때
- 여러 AI 에이전트가 동일 코드베이스를 다루는 환경
주의가 필요한 상황:
- 스펙 품질이 낮으면 타입도 틀립니다. 생성된 클라이언트는 스펙의 정확도에 전적으로 의존합니다.
any로 도배된 스펙을 입력하면any가 나옵니다. - 커스텀 로직은 래퍼 레이어에 작성해야 합니다. 인증 헤더 추가, 에러 변환 같은 건
src/api/아래에 별도로 작성하는 습관이 필요합니다. - Codex CLI 자동 실행 모드를 사용할 때는 네트워크 요청이 포함된 codegen 작업의 승인 방식을 명시적으로 설정해야 합니다. Codex CLI의 승인 및 샌드박스 옵션은
--approval-mode와 관련 샌드박스 플래그로 제어되므로, 원격 스펙을 fetch하는 파이프라인이라면 approval modes 문서를 참고해 실제 동작을 검증한 뒤 도입하세요. - 초기 설정 비용이 있습니다. 코드젠 툴 설정, AGENTS.md 작성, CI 통합까지 하루 정도는 잡아야 합니다.
한 가지 더 — AGENTS.md는 원래 Codex 계열에서 출발했지만, 다른 코딩 에이전트들도 유사한 프로젝트 지침 파일을 참조하는 방향으로 움직이고 있습니다. 공식 표준화 기구로의 이관 여부까지는 단정하기 어렵지만, 멀티벤더 지원이 확산되는 흐름 자체는 관찰 가능합니다. 지금 작성해두면 나중에 도구가 바뀌어도 AGENTS.md의 내용은 대부분 재사용할 수 있을 겁니다.
마무리하며
이 워크플로우의 핵심은 단순합니다. 타입의 소유권을 하나로 통일하고(OpenAPI 스펙), 그 규칙을 사람과 에이전트 모두가 읽을 수 있는 파일에 명문화하는 것(AGENTS.md)입니다.
기술 스택은 바뀝니다. @hey-api/openapi-ts가 다른 툴로 대체될 수도 있고, Codex CLI 대신 다른 에이전트를 쓰게 될 수도 있습니다. 그래도 남는 건 두 가지입니다. openapi.yaml이라는 계약서, 그리고 그 계약을 어떻게 다룰지 명시한 AGENTS.md. 도구는 갈아탈 수 있지만 계약과 규칙은 그대로 이어집니다. 스펙-드리븐 워크플로우가 매력적인 진짜 이유는 여기에 있다고 봅니다.
참고 자료
- openai/codex AGENTS.md — 공식 GitHub
- AGENTS.md for OpenAI Codex: Complete Setup and Configuration Guide 2026
- AGENTS.md Playbook 2026: Codex CLI Hierarchy + Monorepo
- AGENTS.md: the agent instructions file — AgentProtocol
- Typesafe API Code Generation for React in 2026
- OpenAPI to TypeScript: Zero API Drift in 2026
- Which OpenAPI Codegen Should You Choose? — DEV Community
- Codex CLI Complete Reference 2026
- Codex CLI approval policies and sandbox modes explained
- Building Consistent Workflows with Codex CLI & Agents SDK — OpenAI Cookbook