API 파이프라인이 팀을 속이기 시작하는 순간은 보통 이럴 때입니다. 스키마가 바뀌고, 생성된 타입은 아무 문제 없이 업데이트되고, PR은 통과됩니다. 그런데 프론트엔드 누군가는 래퍼가 에러를 타입 캐스팅으로 감춰버린 탓에 여전히 예전 응답 형태를 읽고 있죠. 이건 제너레이터가 인터페이스를 뽑아낼 수 있느냐의 문제가 아니라, OpenAPI TypeScript의 문제입니다.
쓸모 있는 질문은 더 어렵습니다. 스키마, 전송, 검증 사이에 어떤 계약을 맺고 싶은가? 그리고 어느 부분이 런타임으로 새지 않고 빌드 타임에 빠르게 실패해야 하는가? 이걸 OpenAPI TypeScript는 파이프라인 선택의 문제로 틀을 잡고 나면 트레이드오프가 훨씬 선명해지고, 도구가 모든 해결책인 척하는 일도 사라집니다.
목차
- 생성된 타입이 안전한 API와 같지 않은 이유
- OpenAPI 스펙에서 TypeScript 타입 생성하기
- Pure Types, Full Clients, Codegen 미사용 중 선택하기
- Fetch 또는 Axios를 감싸는 얇은 Typed Client 구성하기
- zod, ajv, io-ts로 런타임 검증 추가하기
- CI에 생성·검증·계약 테스트 넣기
- 유지보수 가능한 파이프라인, 성능, 최종 체크리스트
생성된 타입이 안전한 API와 같지 않은 이유
팀원이 선택적 응답 필드를 추가하는 PR을 병합합니다. 생성된 파일은 깔끔하게 업데이트되고, diff는 지루해 보이며, 모두가 넘어갑니다. 그런데 프론트엔드는 '임시로' 캐스팅된 as any을 통해 여전히 구형 구조를 읽다가, 프로덕션이 계약이 변경되지 않은 것처럼 작동하기 시작합니다.
생성된 타입의 함정입니다. TypeScript는 생성된 타입을 소비하는 code만 보호할 수 있습니다, 그리고 전송 계층이 계약을 다시 지우지 않는 경우에만 그렇습니다. OpenAPI 쪽은 스키마를 제공하지만, 모든 호출자가 이를 준수한다는 보장은 주지 않습니다. API 연결을 이해하는 데 대한 논의 단일 도구가 아닌 시스템 간 연결 방식에 대한 대화를 유도하기 때문에 여기에서 유용합니다.
실패가 숨어 있는 곳
가장 흔한 중단점은 특이한 것이 아니라 지루한 것입니다. 스키마 드리프트 OpenAPI 스펙과 배포된 서비스가 서로 일치하지 않을 때 발생합니다. 부분 커버리지 스펙은 해피 패스(happy path)만 모델링하고, 앱은 문서화되지 않은 엣지 케이스에 의존할 때 나타납니다. 직접 작성한 래퍼 타입이 약해지는 경우가 많습니다. 특히 누군가 '빠르게' 움직이려고 사용할 때 any 또는 느슨한 응답 캐스트를 쓸 때입니다.
실무 원칙: 래퍼가 거짓말할 수 있다면, 제너레이터도 구해주지 못합니다.
런타임 갭도 있습니다. TypeScript 타입은 컴파일 후 사라지므로 네트워크로 들어오는 잘못된 형식의 JSON을 거부할 수 없습니다. 네트워크는 에디터가 추론한 타입을 신경 쓰지 않습니다. 그래서 생성된 클라이언트는 더 안전한 API 파이프라인의 한 계층에 불과합니다.
더 큰 운영상의 질문은 개발 편의성만이 아니라 보안과 계약 준수입니다. API 계약이 더 큰 앱 라이프사이클에 어떻게 맞물리는지 체계적으로 살펴보고 싶다면, 이 내부 가이드가 앱 스토어 컴플라이언스를 위한 API 보안 표준 요약이며, 유용한 참고 자료입니다.
성숙한 관점에서 바라보는 openapi typescript 이것입니다. 엄격한 스키마-타입 브리지를 제공해 훌륭하지만, 요청 검증, 런타임 페이로드 형식 강제, 느슨한 래퍼가 전체를 무너뜨리는 것을 막지는 못합니다. 생성기는 쉬운 20%에 불과합니다. 나머지는 파이프라인 설계이며, 여기서 팀은 신뢰를 얻거나 잘못된 확신을 쌓게 됩니다.
OpenAPI 스펙에서 TypeScript 타입 생성하기

가장 가볍고 유용한 설정은 실제 리포지토리 변경을 견디는 것입니다. OpenAPI 스펙을 같은 리포지토리에 두고, 커밋된 타입 파일을 생성하며, 누군가 갱신 단계를 기억하길 기대하기보다 CI에서 드리프트를 가시화하세요. 다음과 같은 명령은 npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts 리뷰어가 다른 소스 변경처럼 검사할 수 있는 결정론적 출력 파일을 제공합니다.
실제로 중요한 플래그
이 -o output 플래그는 생성된 아티팩트를 명확히 드러내므로 중요합니다. --immutable 생성된 타입이 출력에서 readonly 의도를 유지하도록 하고 싶을 때 유용하며, --alphabetize 의미 변화 없는 스키마 순서 변경에도 diff를 안정적으로 유지합니다. --enum 팀이 생성된 타입 표면에서 유니온 대신 enum을 선호할 때 중요합니다.
프로젝트 자체 문서는 범위를 분명히 밝힙니다. 이는 타입 생성기이며, 클라이언트 런타임이나 요청 레이어가 아닙니다. 이 제한은 가볍고 타입 우선인 설정을 원할 때 도움이 됩니다. 저장소는 이 도구의 유지보수 모델도 보여주는데, 이는 문서와 릴리스가 활발하게 유지될 때 오픈소스 툴링이 프로덕션에서 버틸 수 있는 이유 중 하나입니다. 이는 ‘오픈소스 유지보수의 필요성’에서 논의된 바와 같습니다. 이러한 태도를 실용적으로 이해하는 방법은 프로젝트의 GitHub 저장소와 CLI 문서입니다..
생성 과정을 package.json 에 연결해 명령어가 다른 빌드 스크립트와 함께 있도록 한 뒤, 스펙이 변경될 때마다 실행하세요. CI에서는 파일을 재생성하고 git diff 에 변경 사항이 감지되면 실패하도록 설정하세요. 이렇게 하면 계약 변경 사항을 조용한 런타임 리스크가 아닌 가시적인 리뷰 작업으로 전환할 수 있습니다.
스키마 측면도 커맨드 라인만큼 중요합니다. 프로젝트는 compilerOptions.noUncheckedIndexedAccess 어떤 additionalProperties 됩니다 T | undefined, 호출 지점에서 더 안전한 인덱싱을 강제합니다. 또한 사용하는 것을 권장합니다 oneOf 추가 구성과 섞지 않고 그 자체로 사용하고, $defs 배치가 모호할 때는 root에 유지하세요. 잘못 배치된 정의는 생성된 출력에서 사라질 수 있기 때문입니다. 나중에 시간을 아껴줄 디테일이 하나 더 있습니다, openapi-typescript 절대 생성하지 않습니다 any, 따라서 누락된 스키마 세부 정보가 느슨한 타입 아래에 숨지 않고 일찍 드러납니다.
spec을 명시적으로 유지하세요. 그렇지 않으면 generator가 모호함을 그대로 다시 노출합니다.
오래가는 워크플로우는 간단합니다. spec을 버전 관리에 넣고, 빌드 시 재생성하고, 생성된 파일을 커밋하세요. 불일치가 머지되기 전에 타입 체커가 문제를 알리게 하세요. 그러면 나머지 pipeline에 안정적인 contract 경계가 생깁니다.
Pure Types, Full Clients, Codegen 미사용 중 선택하기
| 패턴 | 빌드 타임 | 출력 파일 | Bundle 크기 | 가장 적합 |
|---|---|---|---|---|
| 얇은 래퍼를 사용한 순수 타입 | 빠름 | 적음 | 낮음 | 제어권과 최소한의 런타임 표면을 원하는 팀 |
| 전체 클라이언트 코드 생성 | 느림 | 많음 | 높음 | 빠른 핸드오프와 자동 생성된 오퍼레이션을 원하는 팀 |
| codegen request builder 없음 | 빠름 | 없거나 매우 적음 | 낮음 | 직접 작성한 transport 로직을 선호하는 단일 코드베이스 앱 |
선택은 사실 “어떤 도구가 이기느냐”가 아닙니다. 여러분의 레포와 팀, 그리고 API가 겪는 변경량에 맞는 파이프라인 형태가 중요합니다. 2025년 벤치마크에서 약 75,000 라인, 2 MB, 약 1,200개 operation으로 구성된 대규모 OpenAPI spec에서, openapi-typescript 생성된 결과물을 약 1.5초 만에 평균적으로 처리했습니다. 이는 약 8.0초와 비교됩니다 위해 @hey-api/openapi-ts, 5.5초 위해 Orval, 그리고 18.1초 위해 Kubb, 단일 출력 파일을 생성하는 것과 비교하면 16 위해 hey-api, 2,719 위해 Orval, 그리고 3,877 위해 Kubb (벤치마크 상세 정보).
Pure 타입은 제어를 중시
pure-types 구성은 직접 작성한 request layer와 잘 맞습니다. 런타임을 작게 유지하고 API surface를 단순하게 만들 수 있기 때문입니다. 이는 번들러에 민감한 프론트엔드나 한 팀이 스펙과 컨슈머를 모두 관리하는 앱에서 중요합니다. 개발자 경험이 단순한 문법적 설탕 그 이상이라는 점을 다시 확인하고 싶다면, 개발자 경험 관점은 클라이언트 code가 짧고 명확하며 리뷰하기 쉬울 때 판단하기가 더 수월합니다.
Full client는 빠른 핸드오프에 유리합니다
openapi-generator, hey-api, Orval, 그리고 Kubb 모두 타입 그 이상을 하려 합니다. request 메서드, 모델, plumbing을 함께 생성하고 싶을 때, 특히 백엔드와 프론트엔드 팀 간 대규모 핸드오프에서 유용할 수 있습니다. 비용은 위 벤치마크에서 분명합니다. 생성 파일이 많아지고 런타임 surface가 넓어지며, 스펙이 커질수록 빌드 마찰이 생길 여지가 커집니다.
No codegen은 로컬 리팩터링에 유리합니다
타입이 지정된 request builder와 fetch wrapper는 하나의 코드베이스가 데이터 구조의 양 끝을 관리하고 API 변경 사항이 긴밀하게 조율될 때 효과적입니다. 단점은 유지보수 규율입니다. 프로듀서와 컨슈머 사이에 팀과 레포지토리가 많아질수록, 계약 테스트(contract tests)를 적극적으로 강제하지 않는 한 직접 작성한 request layer가 어긋날 가능성이 커집니다.
핵심 결정 기준은 이념적인 문제가 아닙니다. 번들 예산이 빠듯하다면 순수 타입 방식이 매력적입니다. 팀이 최대한의 스캐폴딩(scaffolding)을 원하고 결과물을 감당할 수 있다면, full client가 설정 시간을 줄여줍니다. 움직이는 요소를 최소화하고 계약(contract)을 가깝게 유지할 수 있다면, no-codegen request builder가 적절한 내부 트레이드오프가 될 수 있습니다.
Fetch 또는 Axios를 감싸는 얇은 Typed Client 구성하기

얇은 wrapper는 제너레이터가 끝나고 애플리케이션 code이 시작되는 지점입니다. Wrapper는 작업마다 함수 하나를 노출하고, 타입이 지정된 params와 query 객체를 받아 호출을 넘겨야 합니다. fetch 또는 주입된 axios 인스턴스로 넘기면 됩니다. 복잡하게 만들 필요는 없습니다. 대부분의 프로덕션 환경에서 이 레이어는 약 30~60줄 생성된 타입이 이미 대부분의 구조를 담고 있기 때문입니다.
잘 통하는 멘탈 모델은 다음과 같습니다:
- Path params 타입 유지 어떤
/users/{id}없으면 호출할 수 없음id. - Query 객체 타입 유지 그래서 선택적 필터가 문자열 범벅이 되지 않습니다.
- 응답 본문의 타입이 유지됩니다 덕분에 code를 파싱할 때 기대하는 좁은 형태를 신뢰할 수 있습니다.
이런 래퍼는 의도적으로 단순해야 합니다. 재시도, 변환, 인증 정책이 다른 곳의 역할이라면 여기서 직접 구현해서는 안 됩니다. 래퍼의 역할은 타입이 지정된 연산을 전송 계층으로 넘기고, 타입이 지정된 결과를 다시 위로 돌려주는 것입니다.
래퍼는 단순하고 의존성은 가볍게 유지하세요. 그렇지 않으면 향후 codegen 변경이 앱 전체로 퍼져나갑니다.
흔한 실패는 불일치를 이렇게 임시방편으로 덮는 것입니다 as any 생성된 타입이 기존 래퍼 시그니처와 맞지 않을 때 말이죠. 이는 빌드 통과와 불안정한 앱을 맞바꾸는 일입니다. 또한 generator가 드러내길 원했던 바로 그 계약 위반을 숨기게 됩니다.
Axios를 선호하는 팀도 패턴은 같고 전송 구현만 달라집니다. 더 간단한 브라우저 측 code를 원하는 팀에게는 fetch 이것만으로 충분한 경우가 많습니다. 중요한 점은 요청 함수가 생성된 경로 타입을 받고, 나중에 가공해야 하는 느슨한 객체가 아닌 타입이 지정된 응답을 반환해야 한다는 것입니다.
이 seam을 잘 활용한다면, openapi typescript 역할 분담이 깔끔해집니다. 스키마는 spec에, 전송은 래퍼에 있고, 앱은 임시방편 요청 code 대신 타입이 지정된 연산을 보게 됩니다.
zod, ajv, io-ts로 런타임 검증 추가하기
TypeScript 타입은 런타임에 사라지며, 네트워크는 에디터의 확신 따위는 신경 쓰지 않습니다. 그래서 안전한 패턴은 "타입을 생성하고 기원한다"가 아니라 "타입을 생성한 후, 신뢰할 수 없는 데이터가 앱에 들어오는 엣지에서 검증한다"입니다. 생성된 스키마가 여전히 진실의 원천(source of truth)이며, 검증 라이브러리인 zod, ajv, 그리고 io-ts 이 컴파일 타임 타입이 처리할 수 없는 경계 검사를 담당합니다.
데이터가 들어오는 지점에서 검증하기
React 앱의 경우, 엣지는 보통 요청이 해결된 직후이고 페이로드가 state에 들어가기 전입니다. 서버의 경우, 페이로드가 데이터베이스에 기록되거나 비즈니스 규칙에 전달되기 전입니다. 규칙은 간단합니다. 검증을 경계에 가깝게 유지하고, 수동 검사를 기능 code에 흩뿌리지 마세요.
어떤 zod shape는 생성된 응답 형식을 대체하지 않으면서 이를 반영할 수 있습니다:
import { z } from "zod";
const WeatherForecastSchema = z.object({
date: z.string(),
temperatureC: z.number(),
summary: z.string().nullable(),
temperatureF: z.number().optional(),
});
이 예제는 스키마가 optional로 표시한 필드를 검증하며, 런타임 체크가 생성기가 생성한 내용과 일치하도록 유지합니다. ajv 서버에서 높은 처리량의 JSON Schema 검증을 원할 때 강력한 선택지이며, io-ts 이미 fp-ts 스타일의 구성 방식을 사용하는 팀에도 적합합니다.
가장 큰 실수는 검증이 너무 늦다는 것입니다. 페이로드가 앱 안으로 먼저 들어오면 타입 시스템은 이미 우회된 상태이고 버그가 숨을 곳이 생깁니다. 다음은 JavaScript 단위 테스트를 위한 가이드로, 이런 사고방식과 잘 맞습니다. 단위 테스트와 경계 검증 모두 잘못된 가정을 일찍 잡아낼 때 가장 효과적이기 때문입니다.
깔끔한 레이어링은 예측 가능합니다. OpenAPI TypeScript는 계약(contract)을 생성하고, 검증기는 런타임 페이로드를 확인하며, 앱의 code은 두 단계를 모두 통과한 데이터만 보게 됩니다. 이는 신뢰할 수 없는 응답을 정적 타입에 맡겨 관리하는 것보다 훨씬 나은 경계입니다.
CI에 생성·검증·계약 테스트 넣기

오래가는 파이프라인은 계약을 제안이 아닌 게이트로 만듭니다. 타입을 재생성하고, 드리프트가 나면 실패시키고, 실행해서 tsc --noEmit머지 전에 mock이나 계약 도구로 API 형태를 검증하세요. 생성기 버전을 고정해 두면, package.json두 엔지니어가 같은 스펙으로 실수로 서로 다른 결과물을 만들어낼 수 없습니다.
간단한 GitHub Actions 구조
실제 워크플로우는 다음과 같습니다:
- 레포지토리나 생성된 소스에서 스펙을 가져옵니다.
- 타입을 다시 생성합니다.
- 다음의 경우 작업을 실패 처리합니다:
git diff변경 사항이 감지되면. - 실행
tsc --noEmit. - Prism 같은 mock server를 사용하거나 Spectral 기반 체크를 통해 contract test를 실행합니다.
contract test와 snapshot test의 핵심 차이는 범위입니다. Snapshot은 파일이 변경되었음을 알려주지만, contract test는 데이터 구조가 스펙대로 동작하는지를 알려줍니다.
백엔드와 프론트엔드 작업이 시점이나 팀 단위로 분리되어 있을 때 mock server는 특히 유용합니다. 하드코딩된 fixture 대신 실제 contract를 확인하면서도, consumer code에게 예측 가능한 API surface를 제공합니다. 지속적 통합 설정 가이드 팀에 여전히 깔끔하고 반복 가능한 CI 베이스라인이 필요하다면 유용한 참고 자료입니다.
generator 버전을 고정하면 codegen pipeline에서 가장 까다로운 문제 중 하나인 눈에 보이지 않는 output skew를 방지할 수 있습니다. 한 개발자는 로컬에서 generator를 업그레이드하고 다른 개발자는 하지 않으면, 생성된 파일이 유의미한 정보 대신 무작위 노이즈가 될 수 있습니다. CI를 통해 이를 원천 차단해야 합니다.
결과는 스키마 변경, 타입 생성, 컴파일러 체크, 컨트랙트 테스트가 서로를 강화하는 파이프라인입니다. 이것이 워크플로우를 신뢰할 수 있게 만듭니다.
유지보수 가능한 파이프라인, 성능, 최종 체크리스트

살아남는 파이프라인은 지루한 거버넌스를 갖춘 파이프라인입니다. 스펙에 버전을 붙이고, code 같은 스키마 변경을 리뷰하며, 제너레이터 버전을 고정하고, breaking change 승인 방식을 문서화하세요. 프로세스가 모호하면 사람들은 이를 우회하고, 결국 생성된 타입은 강제 수단이 아니라 장식이 됩니다.
실제로 효과 있는 성능 레버 몇 가지
스펙이 자주 바뀌지만 이를 사용하는 패키지가 하나뿐인 모노레포에서 증분 생성이 유용합니다. tsc --incremental 반복적인 컴파일 작업을 줄일 수 있고, 프로덕션 빌드에서 불필요한 출력 플래그를 끄면 생성되는 코드의 양을 줄일 수 있습니다. 실제로 가장 큰 성과는 여전히 기술이 아니라 협업에서 나옵니다. 예측 가능한 파이프라인이 영리한 파이프라인보다 더 자주 실행되기 때문입니다.
가까이 두고 볼 만한 체크리스트는 아래와 같습니다:
- 버전 고정: 해당
openapi-typescript버전을package.json에 고정해 환경마다 결과물이 달라지지 않도록 하세요. - 스키마 검토: 스펙 변경을 단순 정리가 아닌 검토 가능한 계약 변경으로 취급하세요.
- 드래프트 감지: CI에서 재생성하고 diff가 있으면 실패 처리하세요.
- 엣지 검증: 신뢰할 수 없는 페이로드가 앱 상태나 영속성 저장소에 도달하기 전에 파싱하세요.
- 계약 테스트: 스키마와 소비자 code가 여전히 일치하는지 증명하는 목(mock) 기반 검사를 실행하세요.
- 호환성 깨짐 정책: 구조 변경을 누가 승인하고 클라이언트는 어떻게 통보받는지 문서화하세요.
이러한 게이트를 포함한 파이프라인은 단순히 타입을 생성하는 것을 넘어 계약의 가시성을 확보합니다. 이 가시성이 팀이 겉보기에만 안전한 파일에 의존하는 것을 막아줍니다.
Capacitor 또는 Electron 앱을 배포하면서 업데이트 파이프라인에 동일한 규율을 적용하고 싶다면, Capgo은 앱 스토어 리뷰를 기다리지 않고 JavaScript, CSS, 카피, 설정, 에셋 수정을 빠르게 배포할 수 있는 실용적인 방법을 제공합니다. 방문하기 Capgo 서명된 bundle, 롤백 보호, 릴리스 제어가 통제력을 잃지 않으면서 속도를 내야 하는 릴리스 프로세스에 어떻게 맞물리는지 확인해 보세요.