API 연동 자동화 2탄 - Orval로 API 연동 자동화하기

API 연동 자동화를 통해 프론트엔드 개발 프로세스를 개선한 경험을 공유하고자 합니다. 기존 API 연동 자동화(feat. OAS codegen) 방식에서 한 단계 더 나아가, 실제 개발에서 필요한 보일러플레이트까지 자동화하는 방법을 공유하고자 합니다.

문제 인식

이전에 API 연동 자동화(feat. OAS codegen)를 통해 기본적인 API 연동 프로세스 자동화는 해결했습니다. 백엔드 개발자의 의도대로 프론트엔드에서 API 연동을 할 수 있게 되어 커뮤니케이션 비용과 휴먼 에러를 크게 줄일 수 있었죠.

하지만 실제 프론트엔드 개발에서는 여전히 반복되는 작업이 필요했습니다:

  • API 모킹을 통한 테스트

  • @tanstack/react-query 같은 서버 상태 관리

  • zod를 활용한 런타임 스키마 검증

결국 기존 OAS codegen만으로는 프론트엔드 개발자가 여전히 많은 보일러플레이트 코드를 작성해야 했습니다.

문제 해결 방식 고민

이 문제를 해결하기 위해 두 가지 방법을 고려하였습니다.

1. 직접 개발하자!

  • 직접 반복되는 생성 코드 script를 개발하는 방법(https://tech.inflab.com/20230613-code-generator-part-1/)

  • 장점

    • 요구사항 100% 커스텀 가능

    • 기존 구현 방법과 호환

    • 자체 구현이므로 다른 의존성이 없음

  • 단점

    • 초기 개발 비용

    • 유지보수 리소스

    • 엣지 케이스 검증 필요

2. 라이브러리를 찾아서 활용하자!

  • Orval이라는 라이브러리를 발견했습니다.

Orval은 openAPI 명세를 기반으로 Typescript model을 자동 생성해주는 도구이다.

  • 장점

    • 프로덕션레벨에서 이미 사용되고 있다.

    • OAS를 쓰는 기존방식에서 벗어나지 않는다.

    • 요구조건(React Query, MSW, Zod)도 만족시켜준다.

  • 단점

    • 새로운 도구의 학습 비용

    • 의존성 추가

    • 커스터마이징 제약

결과적으로는 Orval을 도입해보기로 하였는데요.

사용방식이 같아 러닝커브가 크지 않다고 판단하였고, 초기 도입 비용보다는 장기적인 유지보수성과 안정성을 우선시하였기 때문입니다.

기존 방식 vs 개선된 방식

기존의 방식과 기본적으로는 달라지지 않았습니다. 기본적으로 OAS 스펙의 json을 input으로 output을 만들어낼 수 있었습니다.

주요 개선점

  • 라이브러리 연동

    • @tanstack/react-query

    • msw(@faker-js/faker 포함)

    • zod

    • 이밖에도 다른 라이브러리 연동이 가능합니다(ex. SWR)

  • custom http client

    • 기존: 지원하는 일부 client (typescript-axios)만 지원

    • 개선: custom http client를 지원

도입 방법

FE 설정(orval 설정)

https://orval.dev/quick-start

import { defineConfig } from "orval";

export default defineConfig({
  api: {
    input: {
      target: "openapi.json", // OAS json
    },
    output: {
      mode: "tags-split", // 태그별로 분리
      clean: ["./shared/api/endpoints", "./shared/api/model"], // 재생성시 삭제 폴더
      target: "./shared/api/endpoints", // 생성될 파일 경로
      schemas: "./shared/api/model", // 생성될 모델 경로
      client: "react-query", // TanStack Query
      override: {
        query: { // 생성할 쿼리 종류(활성화)
          useQuery: true,
          useInfinite: true,
          useSuspenseQuery: true,
        },
        mutator: {
          path: "./shared/api/http.ts", // custom http client
          name: "httpClient",
        },
      },
      mock: true, // msw
    },
  },
  zod: {
    input: {
      target: "openapi.json", // OAS json
    },
    output: {
      mode: "tags-split",
      client: "zod", // zod
      target: "./shared/api/endpoints",
      fileExtension: ".zod.ts",
    },
  },
});

package.json 스크립트 추가

{
	...
	"scripts": {
		...
		"generate:api": "orval"	
	},
	...
}

BE에서 OAS json 생성(NestJS 기준)

import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "../app.module";
import { setupSwagger } from "../config/document.config";

async function generateSwagger() {
  const app = await NestFactory.create(AppModule, { logger: false, abortOnError: false });

  const document = await setupSwagger(app);

  const outputPath = join(process.cwd(), "..", "..", "docs", "api.json");
  writeFileSync(outputPath, JSON.stringify(document, null, 2));

  console.log(`Swagger JSON generated at: ${outputPath}`);

  await app.close();
}

generateSwagger().catch(error => {
  console.error("Error generating Swagger:", error);
  process.exit(1);
});

Github Action을 이용한 API 생성 자동화

name: OpenAPI Generator

on:
  pull_request:
    types: [opened, synchronize]
    paths:
      - "apps/api/**" 
    branches:
      - main

jobs:
  generate-api:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup PNPM
        uses: pnpm/action-setup@v2
        with:
          version: 9

      - name: ⚙️ Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".node-version"
          cache: "pnpm"
          cache-dependency-path: "pnpm-lock.yaml"

      - name: Install Dependencies
        run: pnpm install --frozen-lockfile

      - name: Generate documentation (api)
        working-directory: apps/api
        run: pnpm run generate:swagger

      - name: Generate API using Orval
        working-directory: apps/web
        run: pnpm run generate:api

      - name: Commit and push changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "feat(web): update API using oas [skip ci]"
          commit_user_name: "frieeren[bot]"
          commit_user_email: "frieeren[bot]@users.noreply.github.com"
          branch: ${{ github.head_ref }}
          push_options: --force-with-lease

PR

https://github.com/Frieeren/time-align/pull/19

개선된 개발 프로세스

인터페이스 작업을 협의한 이후에는 OAS JSON에서 출력된 API와 MSW mock API를 이용해서 웹 개발과 실제 API 구현을 병렬적으로 진행할 수 있습니다.

실무 팁

  • schema 수정이 필요하면? transformer

    import { defineConfig } from "orval";
    
    export default defineConfig({
      api: {
        input: {
          target: "openapi.json", // OAS json
        },
        output: {
          override: {
            transformer: (verb: GeneratorVerbOptions): GeneratorVerbOptions => {
              if (verb.response?.definition.errors === "void") {
                verb.response.definition.errors = "null";
              }
    
              return verb;
            },
          },
        },
      },
    };        
  • custom fetch 수정이 필요하다면? mutator

    • 공식 예제를 참고하여 커스텀 HTTP 클라이언트를 구성할 수 있습니다.

느낀점

Orval을 도입함으로써 API 연동 프로세스의 반복되는 부분을 자동화하고 백엔드 API 개발과 프론트엔드 개발을 병렬로 진행하는데 도움을 주어 전체적인 개발 효율성이 크게 개선되었습니다. 특히 단순한 API 연동 작업은 1분 이내로 완료할 수 있게 되어, 개발자가 정말 중요한 비즈니스 로직과 사용자 경험 개선에 더 많은 시간을 투자할 수 있게 되었다고 생각합니다.

Reference

Last updated