이 글은 누구를 위한 것인가
- 백엔드 API 변경이 프론트엔드에서 즉시 타입 에러로 잡히길 바라는 팀
- 수동으로 작성한 API 타입 파일 관리에 지쳐 있는 개발자
- OpenAPI 스펙에서 React Query 훅을 자동 생성하고 싶은 팀
들어가며
백엔드가 API 응답 구조를 바꿨는데 프론트엔드가 모르는 채로 운영에 배포됐다. OpenAPI 스펙에서 TypeScript 타입을 자동 생성하면 이 문제를 컴파일 시점에 잡을 수 있다.
이 글은 bluefoxdev.kr의 OpenAPI TypeScript 코드 생성 가이드 를 참고하여 작성했습니다.
1. 코드 생성 도구 비교
[주요 도구 비교]
openapi-typescript:
생성: TypeScript 타입만 (런타임 없음)
크기: 매우 작음
유연성: 높음 (직접 fetch 래핑)
적합: 기존 fetch 클라이언트 + 타입만 필요한 경우
@hey-api/openapi-ts:
생성: 타입 + API 함수 + React Query 훅
플러그인: Zod, tanstack-query, axios 등
적합: 완전한 클라이언트 코드 생성
orval:
생성: 타입 + Axios/fetch 클라이언트
React Query, SWR 훅 생성
적합: React Query 훅 자동화
sw2dts (오래된 도구, 비권장):
생성: TypeScript 인터페이스만
업데이트: 활발하지 않음
[워크플로우]
백엔드 openapi.yaml 업데이트
→ CI에서 자동으로 클라이언트 재생성
→ 타입 불일치 시 빌드 실패
→ PR 코멘트로 변경된 타입 표시
2. openapi-typescript 구현
// 설치
// pnpm add -D openapi-typescript
// pnpm add openapi-fetch
// package.json scripts
// "generate:api": "openapi-typescript openapi.yaml -o src/types/api.ts"
// "generate:api:remote": "openapi-typescript https://api.example.com/openapi.yaml -o src/types/api.ts"
// src/lib/api-client.ts
import createClient from 'openapi-fetch';
import type { paths } from '../types/api'; // 자동 생성된 타입
const client = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 인터셉터 추가
const apiClient = {
...client,
async GET<T extends keyof paths>(
url: T,
...args: Parameters<typeof client.GET<T>>
) {
const token = getAuthToken();
return client.GET(url, {
...args[0],
headers: {
...args[0]?.headers,
...(token && { Authorization: `Bearer ${token}` }),
},
});
},
};
function getAuthToken(): string | null {
return localStorage.getItem('auth_token');
}
export { apiClient };
// 사용 예 - 완전한 타입 추론
async function fetchProducts() {
const { data, error } = await apiClient.GET('/api/products', {
params: {
query: {
category: 'electronics', // 타입 안전: API 스펙 기반
page: 1,
limit: 20,
},
},
});
if (error) {
// error 타입도 API 스펙에서 자동 추론
throw new Error(error.message);
}
// data 타입: API 스펙의 응답 타입 자동 추론
return data.products; // Product[] 타입
}
// POST - 요청 바디 타입 검사
async function createProduct(product: {
name: string;
price: number;
category: string;
}) {
const { data, error } = await apiClient.POST('/api/products', {
body: product, // 스펙과 다른 필드 → 컴파일 에러
});
return data;
}
// @hey-api/openapi-ts - 더 완전한 코드 생성
// pnpm add -D @hey-api/openapi-ts
// openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
client: '@hey-api/client-fetch',
input: './openapi.yaml',
output: {
path: './src/generated',
format: 'prettier',
},
plugins: [
'@hey-api/schemas', // Zod 스키마 생성
'@hey-api/services', // API 함수 생성
{
name: '@hey-api/transformers',
dates: true, // 날짜 문자열 → Date 객체 변환
},
{
name: '@tanstack/react-query', // React Query 훅 생성
queryOptions: true,
},
],
});
// 생성 실행
// npx @hey-api/openapi-ts
// 생성된 파일 예시:
// src/generated/
// index.ts → re-export
// schemas.ts → Zod 스키마
// services.ts → API 함수
// types.ts → TypeScript 타입
// @tanstack/
// react-query.ts → useQuery/useMutation 훅
// 자동 생성된 React Query 훅 사용
import { useGetApiProducts, usePostApiProducts } from './generated/@tanstack/react-query';
function ProductsPage() {
const { data, isLoading } = useGetApiProducts({
query: { category: 'electronics', page: 1, limit: 20 },
});
const createProduct = usePostApiProducts();
return (
<div>
{data?.products.map(p => (
<div key={p.id}>{p.name}</div>
))}
<button onClick={() => createProduct.mutate({
body: { name: '신상품', price: 10000, category: 'electronics' }
})}>
상품 추가
</button>
</div>
);
}
# .github/workflows/api-sync.yml
name: API 타입 동기화 검사
on:
pull_request:
paths:
- 'backend/openapi.yaml'
- 'backend/src/**'
jobs:
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: API 스펙에서 타입 재생성
run: |
cp backend/openapi.yaml frontend/openapi.yaml
cd frontend && pnpm generate:api
- name: 타입 변경 감지
run: |
git diff --exit-code frontend/src/types/api.ts || {
echo "⚠️ API 타입이 변경됐습니다. 자동 생성된 타입을 확인하세요."
git diff frontend/src/types/api.ts
exit 1
}
- name: TypeScript 타입 체크
run: cd frontend && pnpm type-check
마무리
OpenAPI → TypeScript 코드 생성은 "API 스펙이 진실의 근원(Single Source of Truth)"이라는 원칙을 실현한다. 백엔드가 응답 구조를 바꾸면 CI에서 자동으로 타입을 재생성하고 타입 체크 실패로 PR이 블록된다. @hey-api/openapi-ts는 React Query 훅까지 생성해 data fetching 코드 작성을 완전히 없앨 수 있다. 팀에 OpenAPI 스펙을 작성하는 문화가 없다면 이 도구들이 그 문화를 만드는 계기가 된다.