Open API Generator + Next.js

h.Im·2025년 5월 8일

회고록

목록 보기
3/5
post-thumbnail

이전 포스팅(https://velog.io/@fddsgt123/Open-API-Genrator)에서 vue3 프로젝트에 open api generator를 적용하여 api 스펙 관리를 하였는데, Next.js로 토이 프로젝트를 진행하면서 좀 더 심화된 적용을 하게 되어 남기는 개발 회고록.

Vue3 프로젝트에서 open api generator를 적용할 때는 Typescript 활용을 100%까지는 하지 못했었다. 이유는 api input/output dto 정의를 제대로 하지 못했기 때문이다.
이번 프로젝트에서 이전에 비해 개선된 점은 아래와 같다.

  1. api output 중 반드시 존재하는 값과 아닌 값에 대한 유효성 확인 로직 개선
  2. 동적으로 output이 달라지는 경우 intersection type 활용
  3. api input 전달 시 Typescript + VS Code의 자동완성 기능 활용

api output 중 반드시 존재하는 값과 아닌 값에 대한 유효성 확인 로직 개선

<div className="flex flex-wrap gap-2">
	{!groupData?.groups?.length && <div>등록된 그룹이 없습니다.</div>}

	{groupData?.groups.map((group) => (
		<button
			key={group.groupId}
			onClick={() => toggleGroup(group.groupId)}
			className={`px-3 py-1 rounded-full text-sm border transition ${
                  activeGroups.includes(group.groupId)
                    ? 'bg-blue-500 text-white border-blue-500'
                    : 'bg-gray-100 dark:bg-neutral-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
                }`}
              >
			{group.groupName}
		</button>
	))}
</div>

groupData api 호출 결과이고, groups가 output dto에 포함되는 상황이다.
"groups" output을 필수 output으로 정의하지 않는다면 groups가 array 혹은 undefined일 수 있기 때문에 jsx 코드를 위와 같이 작성해야 한다.
하지만 groups를 필수 output으로 지정할 경우 아래와 같이 코드가 간결해진다.

<div className="flex flex-wrap gap-2">
	// groups가 없을 경우 노출될 view 삭제, ?. 연산자 제거하고 map 함수 바로 호출
	{groupData.groups.map((group) => (
		<button
			key={group.groupId}
			onClick={() => toggleGroup(group.groupId)}
			className={`px-3 py-1 rounded-full text-sm border transition ${
                  activeGroups.includes(group.groupId)
                    ? 'bg-blue-500 text-white border-blue-500'
                    : 'bg-gray-100 dark:bg-neutral-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
                }`}
              >
			{group.groupName}
		</button>
	))}
</div>

따라서 이번 프로젝트를 진행하면서
1. 필수 output 지정을 최대한 활용한다.
2. 필수 output이 null이 되지 않도록 exception 처리에 신경쓴다.
위 두 가지에 대해 최대한 신경썼다.


동적으로 output이 달라지는 경우 intersection type 활용

api의 output 타입이 특정 상황에서 동적으로 달라져야 하는 경우는 어떻게 처리할 수 있을까? 가장 간편한 방식은 api output dto 타입을 정의하지 않는 것이다.

// conrInf1는 value1을 갖는 하나의 타입이 됨
conrInf1:
	type: object
    value1:
    	type: string
    ...
    
// conrInf2는 내부의 값들이 정의되지 않아 object 타입이 됨
conrInf2:
	type: object

conrInf2처럼 output dto를 정의하면 타입스크립트로는 any 타입으로 다룰 수 밖에 없다. 따라서 동적으로 달라지는 output dto는 open api generator의 allOf 기능을 사용하여 "특정 타입들 중 하나"임을 보장해 주는 것이 좋다.

components:
  schemas:
    BaseResponse:
      type: object
      properties:
        success:
          type: boolean

    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    UserResponse:
      allOf:
        - $ref: '#/components/schemas/BaseResponse'
        - $ref: '#/components/schemas/User'

위와 같이 UserResponse를 정의하면

export interface BaseResponse {
  success?: boolean;
}

export interface User {
  id?: number;
  name?: string;
}

// UserResponse는 두 타입을 조합한 것
export type UserResponse = BaseResponse & User;

UserResponse는 BaseResponse 혹은 User 타입이 되어, any 타입을 사용하지 않아도 된다. any를 사용하는 것은 Typescript의 활용성을 상당히 저해하므로, allOf 기능을 최대한 활용하였다.


Typescript + VS Code의 자동완성 기능 활용

이전 프로젝트에서는 api input dto를 yaml 파일로 정의할 때 아래와 같이 스펙을 작성하였다.

    UpdateBattleReqDTO:
      type: object
      properties:
        battleId:
          type: integer
          format: int64
          description: 배틀 ID
        title:
          type: string
          description: 제목

위와 같이 작성하는 경우, 타입스크립트 기준으로 아래와 같은 dto 코드가 생성된다.

export interface UpdateBattleReqDTO {
  /**
   * 배틀 ID
   * @type {number}
   * @memberof UpdateBattleReqDTO
   */
  battleId?: number;

  /**
   * 제목
   * @type {string}
   * @memberof UpdateBattleReqDTO
   */
  title?: string;
}

코드로 확인 가능하듯, 모든 input param이 선택적 값이 되어 자동완성 기능을 사용하지 못한다. 따라서 필수 input의 경우 아래와 같이 yaml을 작성하였다.

    UpdateBattleReqDTO:
      type: object
      required:
        - battleId
        - title
      properties:
        battleId:
          type: integer
          format: int64
          description: 배틀 ID
        title:
          type: string
          description: 제목

위와 같이 작성 시 아래와 같은 dto 코드가 생성된다.

export interface UpdateBattleReqDTO {
  /**
   * 배틀 ID
   * @type {number}
   * @memberof UpdateBattleReqDTO
   */
  battleId: number; <- battleId가 필수 input으로 정의됨

  /**
   * 제목
   * @type {string}
   * @memberof UpdateBattleReqDTO
   */
  title: string; <- title이 필수 input으로 정의됨
}

필수 input의 경우, "누락된 속성 추가" 클릭으로 필수 input이 자동 생성되도록 할 수 있어 Typescript의 활용도가 높아진다.

0개의 댓글