Swagger로 React Query 훅 자동 생성하기 (feat. orval)

연어코·2025년 11월 26일

우학동

목록 보기
2/2
post-thumbnail

3줄 요약

  • 자동 생성 덕분에 코드 일관성과 타입 안전성이 올라갔다
  • 백엔드 스펙 변경이 곧바로 FE 코드에 반영되어 싱크 맞추기 쉬워졌다
  • API 연동할 때 '수동 작성'보다 '스펙 기반 자동화'는 시도해볼만 하다

문제가 있습니다. 프론트엔드 개발자는 API가 변경될 때마다 타입 정의, API 함수, React Query 훅을 수동으로 업데이트해야 합니다!

예를 들어, 저는 매번 아래와 같은 코드를 일일이 작성했었습니다.

/** 내 프로필 조회 response type */
export type MyProfileResponse = {
  name: string;
  nickname: string;
  email: string;
  phoneNumber: string;
  studentId: string;
  gender: string;
};

/** 내 프로필 조회 axios api */
export const getMyProfile = async () => {
  const { data } = await api.get<MyProfileResponse>(API_URL.USER.MY_PROFILE);
  return data;
};

/** 내 프로필 조회 query */
export const useGetMyProfileQuery = (
  options?: OmittedQueryOptions<MyProfileResponse>,
) => {
  return useQuery({
    queryKey: [API_URL.USER.MY_PROFILE],
    queryFn: () => getMyProfile(),
    ...options,
  });
};

/** 내 프로필 조회 suspense query */
export const useGetMyProfileSuspenseQuery = (
  options?: OmittedSuspenseQueryOptions<MyProfileResponse>,
) => {
  return useSuspenseQuery({
    queryKey: [API_URL.USER.MY_PROFILE],
    queryFn: () => getMyProfile(),
    ...options,
  });
};

이런 반복적인 작업을 자동화할 수 있다면 참 편할 것 같은데요!

Swagger URL만 있으면 자동으로 위와 같은 인터페이스를 생성해주는 라이브러리가 있다고 합니다.

이번 포스팅에서는 API 스펙 기반 코드 생성기인 orval을 적용하고, before-after 코드와 장단점을 논하고자 합니다.


orval이란?

공식문서에 따르면 orval은 OpenAPI 스펙을 기반으로 클라이언트를 생성해주는 도구라고 해요.

OpenAPI code generator도 여러 개가 있는데, FE 중심에다가 React Query 훅, MSW(여기선 사용 안 함)까지 자동으로 만들어주는 도구라서 이걸 선택하게 됐습니다.

정리하자면 orval의 주요 기능은 아래와 같아요.

  • TypeScript 타입 정의 자동 생성
  • Axios, Fetch 등 HTTP 클라이언트 함수 생성
  • React Query 훅 자동 생성
  • MSW, Mock 예시 자동 생성

이게 어떻게 가능하냐? 하면,

저희는 이미 아래와 같은 OpenAPI 스펙 (Swagger)을 가지고 있어요.

orval을 이용해서 단순히 이것을 파싱하고 -> 코드를 생성해주면 됩니다.


기존 방식의 문제점

1. 수동 작성의 번거로움

앞서 소개한 코드처럼 API 엔드포인트마다 다음을 모두 작성해야 했습니다.

  • 타입 정의
  • API 함수
  • React Query 훅 (useQuery, useSuspenseQuery, useMutation)

2. 유지보수의 어려움

  • API 스펙이 변경되면 타입, 함수, 훅을 모두 수동으로 업데이트
  • queryKey 관리를 별도로 해야 함
  • 일관성 없는 코드 패턴

3. 휴먼 에러 가능성

  • 타입 불일치
  • queryKey 오타
  • 잘못된 API 엔드포인트

orval 설정하기

1. 패키지 설치

pnpm add -D orval

2. orval.config.ts 작성

import { defineConfig } from 'orval';

export default defineConfig({
  woohakdong: {
    input: {
      target: 'https://api.woohakdong.com/v3/api-docs', // OpenAPI JSON URL
    },
    output: {
      mode: 'tags-split', // API 태그별로 파일 분리
      target: 'packages/api/src/generated', // 생성될 위치
      client: 'react-query', // 🔥 react-query 훅 자동 생성
      override: {
        mutator: {
          path: 'packages/api/src/axios.ts', // 커스텀 axios 인스턴스 사용
          name: 'customInstance', // 커스텀 axios 인스턴스 이름
        },
        query: {
          useQuery: true,              // useQuery 훅 생성
          useSuspenseQuery: true,      // useSuspenseQuery 훅 생성
          useMutation: true,           // useMutation 훅 생성
          useInfinite: false,          // useInfiniteQuery는 사용 안 함
          signal: true,                // AbortSignal 지원
        },
      },
      fileExtension: '.ts',
      tsconfig: 'packages/api/tsconfig.json',
      prettier: true,
    },
  },
});

3. package.json 스크립트 추가

{
  "scripts": {
    "generate:api": "orval --config orval.config.ts"
  }
}

4. (옵션) packages/api 패키지 설정

모노레포 환경이라면 공통적으로 생성된 코드를 여러 곳에서 갖다쓸 수 있도록 합니다.

{
  "name": "@workspace/api",
  "exports": {
    "./generated": "./src/generated/index.ts"
  },
  "peerDependencies": {
    "@tanstack/react-query": "^5.0.0"
  }
}

생성된 코드 살펴보기

Before: 수동 작성

// 😫 직접 작성
export const useGetJoinedClubsQuery = (options?) => {
  return useQuery({
    queryKey: [API_URL.CLUB.GET_JOINED_CLUBS],
    queryFn: () => getJoinedClubs(),
    ...options,
  });
};

After: orval 자동 생성

// ✨ orval이 자동 생성 (packages/api/src/generated/club/club.ts)

// 1️⃣ API 함수
export const getJoinedClubs = (signal?: AbortSignal) => {
  return customInstance<ListWrapperClubInfoResponse>({
    url: `/api/clubs`,
    method: 'GET',
    signal,
  });
};

// 2️⃣ QueryKey 생성 함수
export const getGetJoinedClubsQueryKey = () => {
  return [`/api/clubs`] as const;
};

// 3️⃣ QueryOptions 생성 함수
export const getGetJoinedClubsQueryOptions = <TData, TError>(options?) => {
  const queryKey = options?.queryKey ?? getGetJoinedClubsQueryKey();
  const queryFn = ({ signal }) => getJoinedClubs(signal);

  return { queryKey, queryFn, ...options };
};

// 4️⃣ useQuery 훅
export function useGetJoinedClubs<TData, TError>(options?) {
  const queryOptions = getGetJoinedClubsQueryOptions(options);
  const query = useQuery(queryOptions);
  query.queryKey = queryOptions.queryKey;
  return query;
}

// 5️⃣ useSuspenseQuery 훅
export function useGetJoinedClubsSuspense<TData, TError>(options?) {
  const queryOptions = getGetJoinedClubsSuspenseQueryOptions(options);
  const query = useSuspenseQuery(queryOptions);
  query.queryKey = queryOptions.queryKey;
  return query;
}

// 6️⃣ Mutation 훅
export const useUpdateClubInfo = <TError, TContext>(options?) => {
  const mutationOptions = getUpdateClubInfoMutationOptions(options);
  return useMutation(mutationOptions);
};

폴더 구조도 단순화되었습니다

Before:
📁 data/
  📁 club/
    📁 getJoinedClubs/
      📄 fetch.ts       # 수동 작성
      📄 query.ts       # 수동 작성
      📄 type.ts        # 수동 작성
    📁 postRegisterClub/
      📄 post.ts        # 수동 작성
      📄 mutation.ts    # 수동 작성

After:
📁 packages/api/src/generated/
  📁 club/
    📄 club.ts          # ✨ 자동 생성 (모든 훅 포함)
  📄 woohakdongServerAPI.schemas.ts  # ✨ 자동 생성 (타입)

마이그레이션 과정

스펙 변경 시 pnpm generate:api 한 번으로 동기화 가능합니다.

1. OpenAPI 스펙 기반 API 생성

pnpm generate:api

2. 기존 코드 업데이트

Before:

// ❌ 기존 수동 작성 훅
import { useGetJoinedClubsQuery } from '@/data/club/getJoinedClubs/query';

const { data } = useGetJoinedClubsQuery();

After:

// ✅ orval 자동 생성 훅
import { useGetJoinedClubs } from '@workspace/api/generated';

const { data } = useGetJoinedClubs();

장점

개발 생산성이 크게 향상되었어요.

1. 반복 작업 자동화

API 스펙만 있으면 타입부터 훅까지 자동 생성되니 편했습니다.

2. 백엔드와 코드 동기화

OpenAPI 스펙과 코드가 항상 동기화되어서, 백엔드와 싱크 맞추기가 편해졌어요.

3. 타입 안전성

타입 정의, queryKey 자동 관리 등 모든 API에 대해 동일한 패턴을 적용하다보니 수동 업데이트로 인한 휴먼 에러를 방지하기에 좋았습니다.

결과적으로 코드의 일관성이 올라가고 유지보수하기 편해졌습니다.


단점

1. 커스터마이징 제약이 있다

자동 생성해주는 건 좋은데, 만들어진 코드를 커스터마이징하고 싶은 경우가 생깁니다. 이때 생성된 코드를 래핑해서 써야 해요.

특히 MSW도 자동 생성해주는 걸 쓰고 싶었는데, 실제 서비스 흐름에 맞추려면 목데이터 커스터마이징이 필요했습니다. 오버라이드하는 게 번거로워서 지금은 마이그레이션 없이 기존 MSW 코드를 유지한 상태입니다.

2. OpenAPI 스펙에 의존하게 된다

장단점이라고도 볼 수 있는데, 그만큼 백엔드에서 정확한 OpenAPI 스펙을 제공해야 합니다. 그러나 한 번 스펙에 대한 규칙을 정해놓으면 이후의 생산성이 올라간다는 장점이 커질 것 같습니다.


결론

orval을 적용해보니 반복적인 API 코드 작성과 유지보수 비용이 크게 줄어들고, 스펙 변경에도 generate 한 번으로 대응할 수 있어 큰 장점으로 다가왔습니다.

다만, 생성된 코드의 커스터마이징 제약과 OpenAPI 스펙 품질 의존성은 고려해야 할 부분입니다.

그럼에도 불구하고 React Query 중심의 프론트엔드 프로젝트라면 도입 효과가 확실하고, 특히 일관된 패턴과 타입 안전성을 중요하게 생각하는 팀이라면 투자해볼만한 기술이라 느꼈습니다.


동아리 관리 SaaS, 우학동은 현재 사전등록 프로모션 진행 중!
👉 https://www.woohakdong.com

profile
Invisible Treasure

1개의 댓글

comment-user-thumbnail
2025년 11월 28일

와 이런게 있었군요 ㄷ 좋은 글 감사합니다!

답글 달기