프로젝트에서 백엔드 API가 늘어날수록 타입 정의와 API 함수를 손으로 하나씩 작성하는 게 너무 번거로워서, 아예 OpenAPI 스펙을 기반으로 전부 자동 생성하는 파이프라인을 설계하게 됐다.
gen-api 스킬 개요
gen-api는 OpenAPI 스펙을 입력으로 받아 TypeScript 타입, API 함수, DTO까지 자동으로 뽑아주는 일종의 코드 생성 파이프라인이다. 단순히 타입만 만드는 게 아니라, 함수명 규칙이나 파일 구조까지 자동화해서 팀 전체가 일관된 코드 스타일을 유지할 수 있게 해준다.
전체 흐름을 간단히 표현하면 이렇다.
입력 파싱 (tag [env])
→ OpenAPI fetch
→ 타입 생성 (npm run generate:types)
→ 경로 분석
→ 파일 생성 (api.ts / dto.ts / index.ts)
→ 완료 리포트
크게 5단계 프로세스로 구성되어 있고, 각 단계가 독립적으로 동작하도록 설계했다.
1단계: 입력 파싱 — 환경별 OpenAPI fetch
가장 먼저 해야 할 일은 어느 환경(dev / prod)의 스펙을 가져올지 결정하는 것이다. 명령어를 실행할 때 tag와 env를 인자로 넘기면, 그에 맞는 OpenAPI 스펙 URL에서 JSON을 fetch해온다.
// 예시: gen-api auth dev
const [tag, env] = process.argv.slice(2);
const specUrl = env === 'prod' ? PROD_SPEC_URL : DEV_SPEC_URL;
const spec = await fetch(specUrl).then((res) => res.json());
dev 서버와 prod 서버의 스펙이 미묘하게 다를 때가 있기 때문에 입력 상태에 따른 환경변수 분기처리가 필요했다. 특히 응답 스키마가 dev에서만 먼저 추가되는 경우가 많았다.
2단계: 타입 생성 — generate:types 실행
스펙을 받아온 뒤에는 npm run generate:types를 통해 TypeScript 타입을 자동 생성한다. 내부적으로는 openapi-typescript 같은 도구를 활용해서 스펙의 components/schemas를 TypeScript interface로 변환한다.
OpenAPI Spec (JSON)
└─ components.schemas
└─ TypeScript interface 자동 생성
└─ dto.ts에 저장
여기서 한 가지 고민이 생겼는데, 스웨거상에서 백엔드가 응답 데이터를 지정해주지 않아 응답 스키마가 EventResult<T> 같은 제네릭 래퍼 형태라서 실제 데이터 타입을 특정할 수 없는 경우가 있었다. 이럴 때는 일단 ResponseDTO<Record<string, unknown>>으로 임시 처리해두고, 나중에 실제 응답 shape이 확정되면 그때 구체 interface로 갈아끼우는 점진적 타입 안전화 전략을 택했다.
// 응답 스키마가 불명확할 때 임시 처리
export type SomeResponseDTO = ResponseDTO<Record<string, unknown>>;
// 실제 shape 확정 후 교체
export interface SomeData {
id: number;
name: string;
}
export type SomeResponseDTO = ResponseDTO<SomeData>;
처음엔 unknown으로 두는 게 찜찜했는데, 생각해보니 타입 없이 any로 방치하는 것보다 훨씬 낫고, 나중에 교체 포인트도 명확해져서 오히려 마음에 들었다.
3단계: 경로 분석 — feature tag 기반 분류
OpenAPI 스펙의 각 엔드포인트에는 tags 필드가 있다. 이 태그를 기준으로 어떤 feature 폴더에 파일을 생성할지 결정한다.
/api/auth/login → tags: ["auth"] → src/features/auth/
/api/event/list → tags: ["event"] → src/features/event/
태그 하나가 곧 하나의 도메인 폴더로 매핑되는 구조라서, 파일이 어디 있는지 굳이 찾아볼 필요가 없어진다. 백엔드에서 태그를 잘 정의해줄수록 이 단계가 깔끔하게 동작한다.
4단계: 코드 생성 — 함수명과 파일 구조 자동화
기존 프로젝트에서 사용하던 컨벤션을 적용해 API 함수명을 사람이 임의로 짓는 게 아니라, 아래 규칙에 따라 자동으로 생성된다.
함수명 규칙: [method][PrefixPascalCase][SegmentPascalCase]
예시:
GET /api/auth/login → getAuthLogin
POST /api/event/register → postEventRegister
DELETE /api/user/profile → deleteUserProfile
HTTP method를 prefix로, 경로 세그먼트를 PascalCase로 변환해서 이어붙이는 방식이다. 규칙이 단순하다 보니 함수명만 봐도 어떤 API인지 바로 유추가 된다는 게 생각보다 감동적이었다..
출력 파일 구조도 통일되어 있다.
src/features/{tag}/
├── api.ts # API 호출 함수
├── dto.ts # 요청/응답 타입 정의
└── index.ts # 외부로 내보내는 barrel export
기존 프로젝트에 이미 만들어진 폴더가 있을 경우, 기존 파일 패턴(예: rsMap 구조, Data 접미사 규칙 등)을 분석해서 새로 생성하는 코드도 그 패턴에 맞게 맞춰준다. 덕분에 "자동 생성 코드만 스타일이 다르다"는 문제가 생기지 않았다.
5단계: 완료 리포트
모든 파일 생성이 끝나면 어떤 파일이 생성/수정됐는지 리포트를 출력한다.
생성 완료
- src/features/auth/api.ts
- src/features/auth/dto.ts
- src/features/auth/index.ts
생성된 함수 목록
- getAuthLogin
- postAuthSignup
- deleteAuthLogout
이 리포트가 있으면 PR에 첨부하기도 편하고, 리뷰어 입장에서도 "자동 생성된 파일"이라는 걸 바로 알 수 있어서 리뷰 속도가 빨라지는 효과가 있었다.
마치며
이 파이프라인을 도입하고 나서 가장 크게 달라진 건, API 하나 추가될 때마다 "타입은 누가 만들지?", "함수명은 어떻게 짓지?"를 고민하는 시간이 거의 사라졌다는 거다. 물론 응답 스키마가 불명확한 경우엔 임시 타입으로 처리하고 나중에 교체하는 과정이 남아 있지만, 그 흐름 자체가 명확하게 정해져 있으니 크게 부담스럽지 않았다.
백엔드가 OpenAPI 스펙을 성실하게 관리해줄수록 이 파이프라인의 효과가 극대화된다. 그런 의미에서 프론트와 백엔드가 함께 OpenAPI 스펙 품질을 높여가는 문화가 병행되면 더 좋을 것 같다는 생각도 든다. 앞으로는 스펙 변경이 감지될 때 자동으로 파이프라인이 돌도록 CI 연동도 붙여보고 싶다.