Orval은 OpenAPI(Swagger) 스펙을 읽어서 프론트엔드의 API 계층을 대신 써 주는 코드 생성기입니다. 엔드포인트마다 타입이 잡힌 함수나 React Query 훅, 계약과 정확히 일치하는 TypeScript 모델, 그리고 원하면 Faker 데이터로 채운 MSW 목 핸들러까지 만들어 줍니다. 설정 파일 하나를 커밋하고 orval을 실행하면 되고, 스펙이 바뀔 때마다 다시 실행하면 됩니다.
이 시리즈의 첫 글에서 “코드를 쓰기 전에 OpenAPI 계약부터 합의하자"는 이야기를 했습니다. 이번 글은 그 계약이 실제로 본전을 뽑게 해주는 도구에 대한 이야기입니다.
프론트엔드의 API 계층을 손으로 짜는 건, 즐겁다고 말하는 사람을 본 적이 없는 종류의 일입니다. 백엔드 문서에서 타입을 베껴 적고, 마흔 번째 똑같은 요청 래퍼를 쓰고, 3주 뒤에 누군가 필드명을 바꾸면 호출의 절반이 조용히 깨지죠. Orval은 이 일 자체를 없애줍니다. OpenAPI 스펙을 가리키기만 하면 클라이언트 코드를 생성해줍니다. 모든 엔드포인트에 대한 타입이 잡힌 함수, 계약과 정확히 일치하는 모델, 그리고 백엔드가 존재하기도 전에 개발에 쓸 수 있는 목(mock)까지.
프론트엔드 팀이 좋아하는 이유#
목 생성이 은근히 저평가된 부분입니다. 스펙만 합의되면 프론트엔드는 가짜 서버를 띄워놓고 UI 전체를 거기에 대고 만들 수 있습니다. 영원히 “거의 다 됐어요” 상태인 엔드포인트를 기다릴 필요가 없죠. 진짜 백엔드가 나오면 클라이언트의 방향만 돌려주면 대부분 그대로 동작합니다. 양쪽이 같은 문서에서 만들어졌으니까요.
또 하나의 수확은, 지루한 코드가 구조적으로 올바르게 나온다는 점입니다. 손으로 짠 요청 바디에서 필드명을 잘못 치는 사람이 없어집니다. 애초에 손으로 요청 바디를 짜는 사람이 없어졌으니까요.
백엔드 팀도 좋아하는 이유#
스펙이 단일 진실 공급원이 되면 일상적인 대화가 달라집니다. “이 엔드포인트가 뭘 반환하더라?“라는 채팅 스레드 대신, 프론트엔드는 생성된 타입을 읽으면 됩니다. 백엔드 개발자는 덜 끊기고, 피드백은 더 빨리 받습니다. 프론트엔드가 목을 상대로 만들어보다가 API가 어딘가 어색하다고 느끼면, 그걸 알게 되는 시점이 프로덕션 이후가 아니라 아직 고치기 싼 시점입니다.
우리 워크플로에 녹이는 방법#
별다른 건 없습니다. 백엔드가 스펙을 제안하고, 양 팀이 리뷰하고, 프론트엔드는 Orval로 클라이언트와 목을 생성해 개발을 시작하고, 백엔드는 같은 계약에 맞춰 실제 엔드포인트를 구현합니다. 스펙이 바뀌면 양쪽에서 다시 생성하고, 뭐가 깨졌는지는 컴파일러가 정확히 알려줍니다. 사실 이 마지막 부분이 핵심입니다. 프론트와 백엔드 사이의 어긋남이 프로덕션 버그가 아니라 컴파일 에러가 되는 거죠.
설정하기#
개발 의존성으로 설치하고, 생성된 코드가 import할 패키지도 함께 설치합니다. React Query 훅과 목을 쓸 거라면 TanStack Query, MSW, Faker입니다.
npm install --save-dev orval msw @faker-js/faker
npm install @tanstack/react-query그다음 프로젝트 루트에 orval.config.ts를 추가합니다. 아래 설정은 Orval 공식 문서의 예제와 거의 같습니다.
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './openapi.yaml', // a path or a URL
},
output: {
mode: 'single',
target: './src/api/petstore.ts',
schemas: './src/api/model',
client: 'react-query',
httpClient: 'fetch',
mock: true, // MSW handlers plus Faker data
override: {
fetch: { includeHttpResponseReturnType: false },
},
},
},
});무엇이 생성될지는 client가 정합니다. 그냥 fetch나 axios 함수, React Query·Vue Query·Svelte Query·SWR용 훅, Angular 서비스, Zod 스키마 중에서 고를 수 있습니다. includeHttpResponseReturnType: false 줄은 fetch 클라이언트가 { data, status } 대신 응답 바디 자체를 반환하게 하는데, 컴포넌트 입장에서는 대개 이쪽이 편합니다.
명령어를 아무도 외울 필요가 없도록 스크립트도 추가해 둡니다.
{
"scripts": {
"api": "orval"
}
}npm run api를 실행하면 훅과 모델이 생성됩니다. 컴포넌트에서는 여느 React Query 훅처럼 쓰면 되고, 타입은 이미 다 채워져 있습니다.
import { useListPets } from './api/petstore';
export function PetList() {
const { data: pets, isLoading } = useListPets();
if (isLoading) return <p>Loading…</p>;
return (
<ul>
{pets?.map((pet) => <li key={pet.id}>{pet.name}</li>)}
</ul>
);
}스펙에서 필드 이름을 바꾸고 npm run api를 다시 돌리면, pet.name을 쓰는 곳마다 빨간 줄이 그어집니다. 앞에서 말한 어긋남 검사가 바로 이것이고, 돌리는 데 드는 비용은 0입니다.
목을 상대로 개발하기#
mock: true를 켜면 Orval은 엔드포인트마다 MSW 요청 핸들러도 만듭니다. 응답 스키마 모양 그대로의 Faker 데이터를 돌려주는 핸들러들이고, 이걸 전부 모아 반환하는 함수도 하나 생깁니다. 그 목록을 브라우저에서는 MSW의 setupWorker에, 테스트에서는 setupServer에 넘기면, 앱은 이미 계약과 일치하는 가짜 백엔드를 상대로 돌아갑니다. 핸들러마다 오버라이드도 받을 수 있어서, 특정 페이로드나 빈 목록, 에러 상태를 돌려주는 테스트를 누가 픽스처를 손으로 짜지 않아도 만들 수 있습니다.
프론트엔드가 첫날부터 작업을 시작할 수 있는 건 이 덕분입니다. 목은 누군가 따로 관리해야 하는 별도의 산출물이 아닙니다. 클라이언트와 같은 스펙에서 매번 다시 생성되니, 스펙보다 뒤처질 수가 없죠.
Orval과 openapi-generator, openapi-typescript, Hey API 비교#
넷 다 OpenAPI 스펙에서 출발하지만, 멈추는 지점이 다릅니다.
| 생성하는 것 | 잘 맞는 경우 | |
|---|---|---|
| Orval | 타입이 잡힌 클라이언트 함수나 프레임워크 훅(React Query, SWR, Vue, Svelte, Angular), 모델, MSW 목, Zod 스키마 | 설정 하나로 훅과 목을 같이 뽑고 싶은 TypeScript 프론트엔드 |
| openapi-generator | 수십 개 언어의 클라이언트와 서버 스텁(TypeScript 포함) | 같은 스펙을 Java, Kotlin, Swift, TypeScript로 모두 뽑아야 하는 팀 |
| openapi-typescript | 타입만. 작은 타입 fetch 래퍼인 openapi-fetch와 짝을 이룸 | 런타임 코드는 거의 없이 타입만 원할 때 |
Hey API (@hey-api/openapi-ts) | 타입이 잡힌 SDK. TanStack Query, Zod 등을 위한 플러그인 제공 | Orval과 비슷한 영역을 플러그인 시스템 중심으로 풀어낸 도구 |
타입만 필요하다면 openapi-typescript가 가장 가볍고, 딱히 반박할 거리도 없습니다. openapi-generator는 스펙 하나로 여러 언어를 먹여 살려야 할 때 제값을 합니다. TypeScript 출력도 동작은 하지만, 읽다 보면 Java를 먼저 염두에 두고 만든 티가 납니다. Orval과 Hey API 사이에서는 대체로 취향 문제이고, 어느 쪽이 내놓는 코드를 읽는 게 더 편한지로 갈립니다. 제가 계속 Orval을 쓰는 건 목이 훅과 같은 실행에서 함께 나오기 때문이고, 두 팀이 일하는 방식을 바꾼 게 바로 그 기능이었습니다.
화려한 도구는 아닙니다. 그저 팀 사이의 말다툼 한 종류를 통째로 조용히 없애줄 뿐인데, 저는 그 거래라면 언제든 응할 생각입니다.
