<?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>OpenAPI · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/openapi/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/openapi/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>
    <item>
      <title>API 설계 우선(Design-First) 워크플로우: 스프린트마다 OpenAPI 계약 정하기</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-planning-an-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-planning-an-api/</guid>
      <pubDate>Mon, 09 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>API</category>
      <category>OpenAPI</category>
      <category>API Design</category>
      <category>Planning</category>
      <category>Mock Server</category>
      <description>스프린트마다 돌리는 API 설계 우선 개발 프로세스. 백엔드와 프론트엔드 담당자가 OpenAPI 계약에 합의하면 CI가 목 서버를 띄우고, 양쪽이 동시에 개발합니다. YAML 예제 포함.</description>
      <content:encoded><![CDATA[<p>얼마 전 <a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-open-api/" >OpenAPI를 활용한 API 우선 개발</a>에 대해 일반론을 썼는데, 이번 글은 그 실전편입니다. 우리 팀에서 시험 운영 중인 계획 워크플로우와, 거기에 딸린 YAML 예제를 소개합니다.</p>
<p>요약하면, 이 설계 우선 프로세스는 스프린트마다 다음 순서로 돌아갑니다.</p>
<ol>
<li>그 스프린트의 API 작업을 맡을 담당자를 백엔드와 프론트엔드에서 한 명씩 정합니다.</li>
<li>두 사람이 스프린트 요구사항에 맞춰 OpenAPI 스키마를 작성(또는 수정)하고, 양쪽이 동의할 때까지 리뷰합니다.</li>
<li>스키마를 저장소에 커밋합니다. 이제부터 이게 계약입니다.</li>
<li>커밋된 스키마로 CI가 목 서버를 띄웁니다.</li>
<li>프론트엔드는 목 서버를 상대로 개발하고, 그동안 백엔드는 실제 엔드포인트를 구현합니다.</li>
<li>실제 서비스가 배포되면, 거기서 생성된 스펙이 다음 스프린트 스키마의 출발점이 됩니다.</li>
</ol>

<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%96%b4%ec%a9%8c%eb%8b%a4-%ec%8b%9c%ec%9e%91%ed%96%88%eb%82%98" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>발단은 회고였습니다. API 계획이 얼마나 안 보이는지에 대해 피드백이 꽤 직설적이었거든요. 백엔드는 요구사항을 자기들 나름대로 해석해서 엔드포인트를 만들고, 프론트엔드는 응답 형태를 나중에야 알게 되고, 어긋난 부분은 통합 시점에, 그러니까 가장 비싼 순간에 터져 나왔습니다. 그래서 나온 제안이 양쪽을 계획 단계부터 끌어들이고, OpenAPI 스키마를 모든 것의 기준점으로 삼자는 것이었습니다.</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%9c%ec%84%b8%ec%8a%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>매주 백엔드 한 명, 프론트엔드 한 명, 이렇게 대표 두 명을 뽑습니다. 이들의 임무는 스프린트 요구사항을 이해하고 계획용 OpenAPI 스키마를 거기에 맞추는 것입니다. 둘이 함께 스키마를 리뷰하고 안 맞는 부분을 고치다 보면, 만들기 전에 양쪽 모두 정확히 뭘 만들지 알게 됩니다.</p>
<p>수정이 마무리되면 스키마를 저장소에 커밋하고, CI/CD가 그 yaml로 목 API를 생성합니다. 스프린트가 끝나면 다음 스프린트의 API 요구사항을 담은 새 yaml을 만들고, 같은 사이클을 반복합니다.</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%8f%84%ea%b5%ac-%ea%b4%80%eb%a0%a8-%eb%a9%94%eb%aa%a8" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>yaml 시각화는 <a href="https://editor.swagger.io/"  target="_blank" rel="noreferrer">Swagger Editor</a>면 충분합니다. 파일을 올리면 제대로 된 Swagger 문서로 렌더링해서 보여 줍니다.</p>
<p>CI/CD 파이프라인은 yaml을 받아서 목 서버를 띄울 수 있어야 합니다. Stoplight의 <a href="https://github.com/stoplightio/prism"  target="_blank" rel="noreferrer">Prism</a>이 널리 쓰이는 선택지이고, OpenAPI 3.1도 지원합니다.</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 -g @stoplight/prism-cli
</span></span><span class="line"><span class="cl">prism mock openapi.yaml</span></span></code></pre></div></div>
<p>이렇게 하면 스키마의 모든 경로가 <code>http://127.0.0.1:4010</code>에서 서비스되고, 응답으로는 스키마의 <code>examples</code>가 그대로 돌아옵니다. 예를 들어 <code>GET /lists/10</code>을 호출하면 부록에 있는 예시 리스트가 옵니다. 계약도 제대로 지키게 합니다. 예제에서 API 키를 선언해 뒀기 때문에 <code>X-API-Key</code> 헤더가 없는 요청에는 401을 돌려주는데, 프론트엔드가 맞춰 개발해야 할 동작이 바로 이거죠. 컨테이너 안에서 돌릴 때는 바깥에서 접근할 수 있도록 <code>-h 0.0.0.0</code>을 붙이세요. <a href="https://github.com/muonsoft/openapi-mock"  target="_blank" rel="noreferrer">openapi-mock</a>은 응답을 무작위 데이터로 채워 주는, 좀 더 가벼운 대안입니다.</p>
<p>목 서버를 띄우기 전에 스키마를 린트해 두는 것도 좋습니다. 계약이 깨져 있으면 프론트엔드를 헷갈리게 하는 대신 빌드가 실패하니까요. <a href="https://github.com/stoplightio/spectral"  target="_blank" rel="noreferrer">Spectral</a>은 한 줄짜리 룰셋만 있으면 바로 돌아갑니다.</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 -g @stoplight/spectral-cli
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;extends: [&#34;spectral:oas&#34;]&#39;</span> &gt; .spectral.yaml
</span></span><span class="line"><span class="cl">spectral lint openapi.yaml</span></span></code></pre></div></div>
<p>마지막으로, 이 스키마는 어디까지나 계획용 산출물입니다. 실제 API가 배포되면 각 서비스가 자기 openapi.json을 생성해야 하고, 그걸 내려받아 다음 계획에 다시 활용하면 됩니다.</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%99%9c-%ec%9d%b4-%ec%88%98%ea%b3%a0%eb%a5%bc-%ed%95%98%eb%82%98" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>논쟁이 가장 싼 곳으로 옮겨가기 때문입니다. 스키마를 쓰는 자리에 프론트엔드와 백엔드 엔지니어가 같이 앉아 있으면, 오해는 코드가 되기 전에 잡힙니다. API 요구사항은 스프린트 중간에 재협상되는 대신 한 번에 합의되고, 목 서버 덕분에 프론트엔드는 기다리지 않고 바로 시작하고, 중요한 디테일이 빠져나갈 틈도 훨씬 줄어듭니다. 두 쌍의 눈과 문서화된 계약을 다 통과해야 하니까요.</p>
<p>혁명적인 이야기는 하나도 없습니다. 그저 기계가 목으로 되돌려줄 수 있을 만큼 계획을 구체적으로 만들었을 뿐인데, 알고 보니 계획에 필요한 구체성이란 딱 그 정도더군요.</p>

<h2 class="relative group">부록 A - yaml 예제
    <div id="부록-a---yaml-예제" 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%b6%80%eb%a1%9d-a---yaml-%ec%98%88%ec%a0%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">openapi</span><span class="p">:</span><span class="w"> </span><span class="m">3.1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">info</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">MyApp Sprint 3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Planning schema for sprint 3. Drives the mock server only.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">0.3.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">servers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://api-mock.example.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">lists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Saved lists feature (PROJ-123)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">security</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">api_key</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">/lists</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">get</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">lists]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="l">Return the current user&#39;s saved lists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l">getLists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">array</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">items</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/List&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">/lists/{listId}:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">get</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">lists]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="l">Find a list by ID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l">getListById</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">listId</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/List&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;404&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">List not found</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schemas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">List</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">id, title]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">10</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">title</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">Weekend reading]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">itemCount</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">7</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">securitySchemes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">api_key</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">apiKey</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">X-API-Key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">header</span></span></span></code></pre></div></div>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-planning-an-api/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>API 우선 개발(API-First)과 OpenAPI: 프론트엔드와 백엔드를 병렬로 개발하기</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-open-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-open-api/</guid>
      <pubDate>Sun, 08 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>OpenAPI</category>
      <category>backend</category>
      <category>frontend</category>
      <category>API design</category>
      <category>software development</category>
      <category>team collaboration</category>
      <description>API 우선 개발을 실무에서 하는 법: OpenAPI 계약에 먼저 합의하고 목 서버를 띄우면, 프론트엔드가 더 이상 백엔드를 기다리지 않아도 됩니다.</description>
      <content:encoded><![CDATA[<p>지금까지 본 것 중 일정을 가장 확실하게 망치는 건 어려운 엔지니어링이 아닙니다. &ldquo;거의 다 됐다&quot;는 엔드포인트를 프론트엔드가 하염없이 기다리는 상황이죠. 회사에서 정착시킨 해법은 지루하지만 잘 듣습니다. 코드를 쓰기 전에 OpenAPI로 API 계약부터 합의하는 겁니다.</p>

<h3 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="#%ea%b8%b0%eb%8b%a4%eb%a6%bc%ec%9d%98-%ea%b2%8c%ec%9e%84" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>전통적인 흐름은 직렬입니다. 백엔드가 API를 설계하고 구현한 뒤, 엔드포인트가 실제로 동작해야 프론트엔드가 통합을 시작합니다. 백엔드의 지연은 전부 프론트엔드 일정에 그대로 얹히는데, 릴리스 날짜가 적혀 있는 곳이 대개 프론트엔드 일정표입니다.</p>

<h3 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%bd%94%eb%93%9c%eb%b3%b4%eb%8b%a4-%ea%b3%84%ec%95%bd%ec%9d%b4-%eb%a8%bc%ec%a0%80" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>스키마 우선 흐름에서는 양쪽이 거의 동시에 출발합니다. 코드가 한 줄도 없는 시점에 API 구조를 OpenAPI 스키마로 적어 둡니다. 모든 엔드포인트, 요청·응답 모델, 인증 방식, 에러 코드까지요. 이 문서가 프론트엔드와 백엔드 사이의 계약서가 됩니다.</p>
<p>효과는 바로 나타납니다. 만들기 전에 모두가 기능을 같은 모양으로 이해하게 됩니다. 백엔드가 구현 중인 동안 프론트엔드는 UI를 만들 수 있고요. 합의된 단일 진실 공급원 하나만 고치면 되니 변경도 빨리 돕니다. 게다가 스키마에서 바로 목 서버를 생성할 수 있어서, 진짜 엔드포인트가 하나도 없는 단계부터 프론트엔드 테스트가 시작됩니다.</p>

<h3 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%8b%a4%ec%a0%9c%eb%a1%9c-%eb%8f%8c%eb%a6%ac%eb%8a%94-%ec%88%9c%ec%84%9c" aria-label="앵커">#</a>
    </span>
    
</h3>
<ol>
<li>요구사항을 모읍니다. 뭘 만들든 똑같죠.</li>
<li>스키마를 설계합니다. 엔드포인트, 요청/응답 모델, 인증, 에러 코드.</li>
<li>프론트엔드와 백엔드 양쪽이 함께 리뷰합니다. 계약이 제값을 하는 순간이 바로 여기입니다.</li>
<li>목 서버를 생성하고(Swagger, Postman 둘 다 됩니다) 프론트엔드는 그걸 상대로 통합을 진행합니다.</li>
<li>백엔드는 같은 스키마에 맞춰 진짜 엔드포인트를 구현합니다.</li>
<li>완성되는 대로 목을 진짜 엔드포인트로 갈아 끼우고, 계약이 지켜지는지 계속 테스트합니다.</li>
<li>대화를 이어갑니다. 스키마는 살아 있는 문서지 석판이 아니니까요.</li>
</ol>

<h3 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="#%ea%b1%b8%eb%a6%ac%eb%8a%94-%eb%b6%80%eb%b6%84" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>공짜는 아닙니다. 꼼꼼한 스키마를 미리 쓰는 데는 실제로 시간이 듭니다. 요구사항이 바뀌면 스키마부터 고쳐야 하고, 그 파장은 두 팀 모두에 미칩니다. 그리고 전원이 OpenAPI와 주변 도구에 최소한의 감은 갖고 있어야 하죠.</p>
<p>그래도 이 선행 투자가, 그 덕에 막을 수 있는 통합 지옥에 지는 걸 본 적이 없습니다. 프론트엔드는 첫날부터 달리기 시작하고, 논쟁은 깨진 빌드가 아니라 문서 위에서 벌어지게 됩니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-open-api/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
