<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>API Development · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/api-development/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Mon, 26 Oct 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/api-development/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Orval 사용법: OpenAPI 스펙에서 타입이 잡힌 API 클라이언트와 목(mock) 생성하기</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2024/2024-04-10-openapitools/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2024/2024-04-10-openapitools/</guid>
      <pubDate>Wed, 10 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>API Development</category>
      <category>Orval</category>
      <category>OpenAPI</category>
      <category>TypeScript</category>
      <category>React Query</category>
      <category>MSW</category>
      <category>Frontend</category>
      <category>Backend</category>
      <category>Teamwork</category>
      <category>Efficiency</category>
      <description>Orval은 OpenAPI 스펙을 읽어 타입이 잡힌 TypeScript 클라이언트와 React Query 훅, MSW 목을 생성해 줍니다. API 계층을 손으로 짤 일이 없어지죠. 설정 방법, 목이 동작하는 방식, 그리고 openapi-generator, openapi-typescript, Hey API와의 비교까지 정리했습니다.</description>
      <content:encoded><![CDATA[<p>Orval은 OpenAPI(Swagger) 스펙을 읽어서 프론트엔드의 API 계층을 대신 써 주는 코드 생성기입니다. 엔드포인트마다 타입이 잡힌 함수나 React Query 훅, 계약과 정확히 일치하는 TypeScript 모델, 그리고 원하면 Faker 데이터로 채운 MSW 목 핸들러까지 만들어 줍니다. 설정 파일 하나를 커밋하고 <code>orval</code>을 실행하면 되고, 스펙이 바뀔 때마다 다시 실행하면 됩니다.</p>
<p>이 시리즈의 첫 글에서 &ldquo;코드를 쓰기 전에 OpenAPI 계약부터 합의하자&quot;는 이야기를 했습니다. 이번 글은 그 계약이 실제로 본전을 뽑게 해주는 도구에 대한 이야기입니다.</p>
<p>프론트엔드의 API 계층을 손으로 짜는 건, 즐겁다고 말하는 사람을 본 적이 없는 종류의 일입니다. 백엔드 문서에서 타입을 베껴 적고, 마흔 번째 똑같은 요청 래퍼를 쓰고, 3주 뒤에 누군가 필드명을 바꾸면 호출의 절반이 조용히 깨지죠. Orval은 이 일 자체를 없애줍니다. OpenAPI 스펙을 가리키기만 하면 클라이언트 코드를 생성해줍니다. 모든 엔드포인트에 대한 타입이 잡힌 함수, 계약과 정확히 일치하는 모델, 그리고 백엔드가 존재하기도 전에 개발에 쓸 수 있는 목(mock)까지.</p>

<h2 class="relative group">프론트엔드 팀이 좋아하는 이유
    <div id="프론트엔드-팀이-좋아하는-이유" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%94%84%eb%a1%a0%ed%8a%b8%ec%97%94%eb%93%9c-%ed%8c%80%ec%9d%b4-%ec%a2%8b%ec%95%84%ed%95%98%eb%8a%94-%ec%9d%b4%ec%9c%a0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>목 생성이 은근히 저평가된 부분입니다. 스펙만 합의되면 프론트엔드는 가짜 서버를 띄워놓고 UI 전체를 거기에 대고 만들 수 있습니다. 영원히 &ldquo;거의 다 됐어요&rdquo; 상태인 엔드포인트를 기다릴 필요가 없죠. 진짜 백엔드가 나오면 클라이언트의 방향만 돌려주면 대부분 그대로 동작합니다. 양쪽이 같은 문서에서 만들어졌으니까요.</p>
<p>또 하나의 수확은, 지루한 코드가 구조적으로 올바르게 나온다는 점입니다. 손으로 짠 요청 바디에서 필드명을 잘못 치는 사람이 없어집니다. 애초에 손으로 요청 바디를 짜는 사람이 없어졌으니까요.</p>

<h2 class="relative group">백엔드 팀도 좋아하는 이유
    <div id="백엔드-팀도-좋아하는-이유" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b0%b1%ec%97%94%eb%93%9c-%ed%8c%80%eb%8f%84-%ec%a2%8b%ec%95%84%ed%95%98%eb%8a%94-%ec%9d%b4%ec%9c%a0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>스펙이 단일 진실 공급원이 되면 일상적인 대화가 달라집니다. &ldquo;이 엔드포인트가 뭘 반환하더라?&ldquo;라는 채팅 스레드 대신, 프론트엔드는 생성된 타입을 읽으면 됩니다. 백엔드 개발자는 덜 끊기고, 피드백은 더 빨리 받습니다. 프론트엔드가 목을 상대로 만들어보다가 API가 어딘가 어색하다고 느끼면, 그걸 알게 되는 시점이 프로덕션 이후가 아니라 아직 고치기 싼 시점입니다.</p>

<h2 class="relative group">우리 워크플로에 녹이는 방법
    <div id="우리-워크플로에-녹이는-방법" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9a%b0%eb%a6%ac-%ec%9b%8c%ed%81%ac%ed%94%8c%eb%a1%9c%ec%97%90-%eb%85%b9%ec%9d%b4%eb%8a%94-%eb%b0%a9%eb%b2%95" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>별다른 건 없습니다. 백엔드가 스펙을 제안하고, 양 팀이 리뷰하고, 프론트엔드는 Orval로 클라이언트와 목을 생성해 개발을 시작하고, 백엔드는 같은 계약에 맞춰 실제 엔드포인트를 구현합니다. 스펙이 바뀌면 양쪽에서 다시 생성하고, 뭐가 깨졌는지는 컴파일러가 정확히 알려줍니다. 사실 이 마지막 부분이 핵심입니다. 프론트와 백엔드 사이의 어긋남이 프로덕션 버그가 아니라 컴파일 에러가 되는 거죠.</p>

<h2 class="relative group">설정하기
    <div id="설정하기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%84%a4%ec%a0%95%ed%95%98%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>개발 의존성으로 설치하고, 생성된 코드가 import할 패키지도 함께 설치합니다. React Query 훅과 목을 쓸 거라면 TanStack Query, MSW, Faker입니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install --save-dev orval msw @faker-js/faker
</span></span><span class="line"><span class="cl">npm install @tanstack/react-query</span></span></code></pre></div></div>
<p>그다음 프로젝트 루트에 <code>orval.config.ts</code>를 추가합니다. 아래 설정은 Orval 공식 문서의 예제와 거의 같습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;orval&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">petstore</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">input</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./openapi.yaml&#39;</span><span class="p">,</span> <span class="c1">// a path or a URL
</span></span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">output</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mode</span><span class="o">:</span> <span class="s1">&#39;single&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./src/api/petstore.ts&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">schemas</span><span class="o">:</span> <span class="s1">&#39;./src/api/model&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">client</span><span class="o">:</span> <span class="s1">&#39;react-query&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">httpClient</span><span class="o">:</span> <span class="s1">&#39;fetch&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mock</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// MSW handlers plus Faker data
</span></span></span><span class="line"><span class="cl">      <span class="nx">override</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fetch</span><span class="o">:</span> <span class="p">{</span> <span class="nx">includeHttpResponseReturnType</span>: <span class="kt">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>무엇이 생성될지는 <code>client</code>가 정합니다. 그냥 <code>fetch</code>나 <code>axios</code> 함수, React Query·Vue Query·Svelte Query·SWR용 훅, Angular 서비스, Zod 스키마 중에서 고를 수 있습니다. <code>includeHttpResponseReturnType: false</code> 줄은 fetch 클라이언트가 <code>{ data, status }</code> 대신 응답 바디 자체를 반환하게 하는데, 컴포넌트 입장에서는 대개 이쪽이 편합니다.</p>
<p>명령어를 아무도 외울 필요가 없도록 스크립트도 추가해 둡니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;scripts&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;api&#34;</span><span class="p">:</span> <span class="s2">&#34;orval&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>npm run api</code>를 실행하면 훅과 모델이 생성됩니다. 컴포넌트에서는 여느 React Query 훅처럼 쓰면 되고, 타입은 이미 다 채워져 있습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useListPets</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./api/petstore&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">PetList() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span>: <span class="kt">pets</span><span class="p">,</span> <span class="nx">isLoading</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useListPets</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="nx">Loading</span><span class="err">…</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nx">pets</span><span class="o">?</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pet</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nt">li</span> <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">id</span><span class="p">}&gt;{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">li</span><span class="p">&gt;)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>스펙에서 필드 이름을 바꾸고 <code>npm run api</code>를 다시 돌리면, <code>pet.name</code>을 쓰는 곳마다 빨간 줄이 그어집니다. 앞에서 말한 어긋남 검사가 바로 이것이고, 돌리는 데 드는 비용은 0입니다.</p>

<h2 class="relative group">목을 상대로 개발하기
    <div id="목을-상대로-개발하기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%aa%a9%ec%9d%84-%ec%83%81%eb%8c%80%eb%a1%9c-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p><code>mock: true</code>를 켜면 Orval은 엔드포인트마다 MSW 요청 핸들러도 만듭니다. 응답 스키마 모양 그대로의 Faker 데이터를 돌려주는 핸들러들이고, 이걸 전부 모아 반환하는 함수도 하나 생깁니다. 그 목록을 브라우저에서는 MSW의 <code>setupWorker</code>에, 테스트에서는 <code>setupServer</code>에 넘기면, 앱은 이미 계약과 일치하는 가짜 백엔드를 상대로 돌아갑니다. 핸들러마다 오버라이드도 받을 수 있어서, 특정 페이로드나 빈 목록, 에러 상태를 돌려주는 테스트를 누가 픽스처를 손으로 짜지 않아도 만들 수 있습니다.</p>
<p>프론트엔드가 첫날부터 작업을 시작할 수 있는 건 이 덕분입니다. 목은 누군가 따로 관리해야 하는 별도의 산출물이 아닙니다. 클라이언트와 같은 스펙에서 매번 다시 생성되니, 스펙보다 뒤처질 수가 없죠.</p>

<h2 class="relative group">Orval과 openapi-generator, openapi-typescript, Hey API 비교
    <div id="orval과-openapi-generator-openapi-typescript-hey-api-비교" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#orval%ea%b3%bc-openapi-generator-openapi-typescript-hey-api-%eb%b9%84%ea%b5%90" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>넷 다 OpenAPI 스펙에서 출발하지만, 멈추는 지점이 다릅니다.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>생성하는 것</th>
					<th>잘 맞는 경우</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Orval</strong></td>
					<td>타입이 잡힌 클라이언트 함수나 프레임워크 훅(React Query, SWR, Vue, Svelte, Angular), 모델, MSW 목, Zod 스키마</td>
					<td>설정 하나로 훅과 목을 같이 뽑고 싶은 TypeScript 프론트엔드</td>
			</tr>
			<tr>
					<td><strong>openapi-generator</strong></td>
					<td>수십 개 언어의 클라이언트와 서버 스텁(TypeScript 포함)</td>
					<td>같은 스펙을 Java, Kotlin, Swift, TypeScript로 모두 뽑아야 하는 팀</td>
			</tr>
			<tr>
					<td><strong>openapi-typescript</strong></td>
					<td>타입만. 작은 타입 fetch 래퍼인 <code>openapi-fetch</code>와 짝을 이룸</td>
					<td>런타임 코드는 거의 없이 타입만 원할 때</td>
			</tr>
			<tr>
					<td><strong>Hey API</strong> (<code>@hey-api/openapi-ts</code>)</td>
					<td>타입이 잡힌 SDK. TanStack Query, Zod 등을 위한 플러그인 제공</td>
					<td>Orval과 비슷한 영역을 플러그인 시스템 중심으로 풀어낸 도구</td>
			</tr>
	</tbody>
</table>
<p>타입만 필요하다면 openapi-typescript가 가장 가볍고, 딱히 반박할 거리도 없습니다. openapi-generator는 스펙 하나로 여러 언어를 먹여 살려야 할 때 제값을 합니다. TypeScript 출력도 동작은 하지만, 읽다 보면 Java를 먼저 염두에 두고 만든 티가 납니다. Orval과 Hey API 사이에서는 대체로 취향 문제이고, 어느 쪽이 내놓는 코드를 읽는 게 더 편한지로 갈립니다. 제가 계속 Orval을 쓰는 건 목이 훅과 같은 실행에서 함께 나오기 때문이고, 두 팀이 일하는 방식을 바꾼 게 바로 그 기능이었습니다.</p>
<p>화려한 도구는 아닙니다. 그저 팀 사이의 말다툼 한 종류를 통째로 조용히 없애줄 뿐인데, 저는 그 거래라면 언제든 응할 생각입니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-10-openapitools/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
