[TS] openapi-typescript 확인하기

장동균·2025년 4월 13일
0

Basic

스웨거에서 반환하는 두 종류의 json (swagger 2.0과 openapi 3.0)

  1. Swagger는 원래 기업이 만든 독립적인 프로젝트였다.
    Swagger는 오픈소스였지만, 공식적인 국제 표준이 아니었음
    Swagger 2.0까지는 “SmartBear Software”에서 직접 관리

  2. 2015년에 OpenAPI Initiative(OAI)가 출범하면서 표준화됨
    Google, Microsoft, IBM 등 여러 대기업이 참여하여 OpenAPI 표준을 만들기로 결정
    Swagger 프로젝트를 기반으로 OpenAPI 3.0이 탄생

  3. Swagger 2.0은 표준 이전의 마지막 버전이었음
    OpenAPI 3.0부터는 기존 Swagger 2.0의 개념을 표준화하여 “OpenAPI”라는 이름으로 변경
    하지만 기존 Swagger 2.0을 OpenAPI 2.0으로 부르지는 않음

(https://spec.openapis.org 가장 최신은 3.1.1이다.)

• Swagger 2.0은 응답을 schema로 바로 정의
• OpenAPI 3.0은 content를 사용하여 application/json을 명확하게 지정하고, $ref를 활용해 응답 객체를 components에서 관리 가능
• OpenAPI 3.0이 더 유연하고 확장 가능하며, 재사용성이 뛰어남


openapi: 3.1.0
paths:
  /pets:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pets'
components:
  schemas:
    Pet:
      required:
        - id
        - name
      properties:
        id:
          type:
            - integer
          format: int64
        name:
          type: string
        none:
          type: 'null'
        tag:
          type:
            - 'null'
            - string
            - integer
          examples:
            - 123
        arr:
          type: array
          $comment: Array without items keyword
        either:
          type: ['string', 'null']
    Pets:
      type: array
      items:
        $ref: '#/components/schemas/Pet'

해당 형태의 yaml이 있을 때, 이 yaml 파일은 어떤 순서로 openapi-typescript에 의해 변환되는지 확인한다.


openapiTS 함수에서 시작한다.

이때 넘어오는 source는 URL 객체이다.

// source 예시
URL {
  href: 'file:///Users/miro/Desktop/typescript-codegen/simple.yaml',
  origin: 'null',
  protocol: 'file:',
  username: '',
  password: '',
  host: '',
  hostname: '',
  port: '',
  pathname: '/Users/miro/Desktop/typescript-codegen/simple.yaml',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

validateAndBundle 함수에 source 인자가 넘어간다.

validateAndBundle의 주요 기능은 3가지이다.

  1. 설정된 url이 반환하는 정보를 가져온다. (parseSchema)
  2. json 객체화 시킨다. 이때 옵션을 통해 reference 처리 또한 가능하다. (bundle)
  3. 에러 처리 (단순히 로그찍는 정도) (_processProblems)
{
  openapi: '3.1.0',
  paths: {
    '/pets': {
      get: {
        responses: {
          '200': {
            content: {
              'application/json': {
                schema: {
                  $ref: '#/components/schemas/Pets'
                }
              }
            }
          }
        }
      }
    }
  },
  components: {
    schemas: {
      Pet: {
        required: ['id', 'name'],
        properties: {
          id: {
            type: ['integer'],
            format: 'int64'
          },
          name: {
            type: 'string'
          },
          none: {
            type: 'null'
          },
          tag: {
            type: ['null', 'string', 'integer'],
            examples: [123]
          },
          arr: {
            type: 'array',
            $comment: 'Array without items keyword'
          },
          either: {
            type: ['string', 'null']
          }
        }
      },
      Pets: {
        type: 'array',
        items: {
          $ref: '#/components/schemas/Pet'
        }
      }
    }
  }
}

validateAndBundle 함수에 의해 기존 yaml은 json화 된다.
이때 bundle 함수의 dereference 옵션을 true로 준다면 ref 처리를 해제할 수 있다.

{
  openapi: '3.1.0',
  paths: {
    '/pets': {
      get: {
        responses: {
          '200': {
            content: {
              'application/json': {
                schema: {
                   type: 'array',
					  items: {
					    required: ['id', 'name'],
					    properties: {
					      id: {
					        type: ['integer'],
					        format: 'int64'
					      },
					      name: {
					        type: 'string'
					      },
					      // ... 나머지 속성들
					    }
					}
                }
              }
            }
          }
        }
      }
  }
}

이후 transformSchema는 반환된 json 파일을 typescript AST 트리로 변환한다. 이 과정에서 typescript compiler API를 사용한다.

[
  NodeObject {
    pos: -1,
    end: -1,
    kind: 264,
    id: 0,
    flags: 16,
    modifierFlagsCache: 0,
    transformFlags: 1,
    parent: undefined,
    original: undefined,
    emitNode: undefined,
    symbol: undefined,
    localSymbol: undefined,
    modifiers: [
      TokenObject {
        pos: -1,
        end: -1,
        kind: 95,
        id: 0,
        flags: 16,
        transformFlags: 0,
        parent: undefined,
        emitNode: undefined
      },
      pos: -1,
      end: -1,
      hasTrailingComma: false,
      transformFlags: 0
    ],
    name: IdentifierObject {
      pos: -1,
      end: -1,
      kind: 80,
      id: 0,
      flags: 16,
      transformFlags: 0,
      parent: undefined,
      emitNode: undefined,
      escapedText: 'paths',
      jsDoc: undefined,
      flowNode: undefined,
      symbol: undefined
    },
  // ...
]

노드의 구성은 AST Viewer에서 확인한다.


완성된 AST 트리는 astToString 함수에 의해 타입 파일로 만들어진다.

export interface paths {
    "/pets": {
        parameters: {
            query?: never;
            header?: never;
            path?: never;
            cookie?: never;
        };
        get: {
            parameters: {
                query?: never;
                header?: never;
                path?: never;
                cookie?: never;
            };
            requestBody?: never;
            responses: {
                200: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": components["schemas"]["Pets"];
                    };
                };
            };
        };
        put?: never;
        post?: never;
        delete?: never;
        options?: never;
        head?: never;
        patch?: never;
        trace?: never;
    };
}
export type webhooks = Record<string, never>;
export interface components {
    schemas: {
        Pet: {
            /** Format: int64 */
            id: number;
            name: string;
            none?: null;
            tag?: null | string | number;
            arr?: unknown[];
            either?: string | null;
        };
        Pets: components["schemas"]["Pet"][];
    };
    responses: never;
    parameters: never;
    requestBodies: never;
    headers: never;
    pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;

변환 과정 분석 중 알게된 내용들

라이브러리에 대한 직접적인 수정이 필요한 경우

yarn berry의 경우 yarn patch를 사용하면 된다.
반면 npm의 경우 node_modules가 노출되어 있기 때문에 node_modules 내부의 라이브러리를 직접 수정하면 된다. 하지만 이 방법은 특정 라이브러리에 대한 수정을 유지할 수 없다는 단점이 존재한다.

이럴 때 patch-package를 사용해볼 수 있다.

node_modules 내부 코드를 수정한 이후
npx patch-package ${라이브러리 이름} 을 실행하면

patchs/{라이브러리 이름} 경로에 파일이 생기고 이곳에 수정된 내역이 기록된다.

diff --git a/node_modules/openapi-typescript/dist/index.js b/node_modules/openapi-typescript/dist/index.js
index 874c6af..9a08c69 100644
--- a/node_modules/openapi-typescript/dist/index.js
+++ b/node_modules/openapi-typescript/dist/index.js
@@ -24,6 +24,7 @@ export * from "./types.js";
 export const COMMENT_HEADER = `/**
  * This file was auto-generated by openapi-typescript.
  * Do not make direct changes to the file.
+ * 커스텀2223
  */
 `;

scripts에 "postinstall": "patch-package"를 추가하면 모듈이 설치될 때마다 이 수정사항은 자동으로 반영된다.


npx

  1. npm i -D openapi-typescript typescript
  2. npx openapi-typescript https://myapi.dev/api/v1/openapi.yaml -o ./path/to/my/schema.d.ts
    npm으로 라이브러리를 설치하고 npx로 실행시키는 이유에 대해 궁금해졌다. (왜 이제서야 궁금해졌지...)

먼저 npx(node package execute)에 대해 확인한다.

npx는 Node.js 설치 시 자동으로 함께 설치된다. (npm 5.2.0 이상 부터)

다음의 순서로 동작한다.
1. 로컬 node_modules/.bin에서 실행 파일 검색
2. 없다면 전역 설치된 패키지 검색 (.nvm/versions/node/${버전}/bin 경로)
3. 없다면 npm 레지스트리에서 패키지 다운로드 후 실행 (다운로드된 패키지는 ~/.npm/_npx 경로에 저장)

즉 npm을 통해 로컬 node_modules/.bin에 실행 파일을 설치하고, npx를 통해 이 파일을 실행시킨 것이다.

그렇다면 .bin 폴더에 실행 파일은 어떻게 설치 될까?

node_modules/.bin 폴더는 npm 패키지의 실행 가능한 바이너리(명령어) 파일들이 심볼릭 링크로 저장되는 특별한 디렉토리이다. 이 폴더의 주요 기능과 특징은 다음과 같다.

  • 실행 가능한 명령어 제공
    npm 패키지가 package.json의 bin 필드에 명령어를 정의하면, 이 명령어들이 .bin 폴더에 심볼릭 링크로 설치된다.

  • npm scripts와의 연동
    package.json의 scripts 섹션에서 명령어를 사용할 때, npm은 자동으로 .bin 폴더의 경로를 PATH에 추가한다. 덕분에 scripts에서는 전체 경로 없이 명령어 이름만으로 실행이 가능하다.
    ex. scripts: {"build": "tsc"} (node_modules/.bin 폴더의 tsc 파일이 실행된다.)

  "bin": {
    "openapi-typescript": "bin/cli.js"
  },

openapi-typescript의 package.json에도 다음과 같은 bin이 존재한다.
node_modules/openapi-typescript/bin/cli.jsnode_modules/.bin/openapi-typescript로 심링크가 생성된다.


Yarn PnP 환경에서는?

Yarn PnP(Plug'n'Play)와, 일반적인 node_modules 방식에서 npx 명령어의 작동 방식은 상당히 다르다.
Yarn PnP에서는 node_modules 폴더가 존재하지 않으므로 다른 방식으로 작동한다.

Yarn PnP는 .pnp.js 파일을 생성하여 모든 패키지의 위치 정보를 저장한다.
Node.js의 모듈 로더가 이 정보를 사용하여 패키지의 실제 위치를 찾는다.

bin 경로 가상화:
npx 명령어가 실행되면 Yarn의 PnP 런타임이 개입한다.
.pnp.js 파일에서 package.json의 bin 필드에 정의된 실행 파일 경로를 찾는다.
실제 파일 시스템 경로로 변환하여 실행한다.

호환성 레이어:
Yarn PnP는 기존 도구와의 호환성을 위해 약간의 "shim"을 제공한다.
이 호환성 레이어가 npx와 같은 npm 도구가 PnP 환경에서 작동할 수 있게 돕는다.

주의사항
일부 경우에는 npx가 Yarn PnP와 완벽하게 호환되지 않을 수 있습니다. 이럴 때는 Yarn의 자체 명령어를 사용하는 것이 더 안정적입니다:
yarn dlx: npx와 유사하지만 Yarn PnP에 최적화됨
yarn exec: 로컬에 설치된 패키지의 바이너리 실행

예시:

# npx 대신
yarn dlx openapi-typescript ./simple.yaml -o ./path/to/my/schema.d.ts
# 또는
yarn exec openapi-typescript ./simple.yaml -o ./path/to/my/schema.d.ts

이렇게 Yarn PnP 환경에서는 .pnp.js 파일이 중심적인 역할을 하며, 모든 패키지 위치 및 bin 스크립트 정보를 저장하고 제공한다.


@redocly/openapi-core
typescript compiler API

openapi 기반의 타입스크립트 파일 생성 기능을 만들기 위해서는 이 두가지를 잘 확인할 필요가 있겠다.

profile
프론트 개발자가 되고 싶어요

1개의 댓글

comment-user-thumbnail
2025년 6월 17일

스웨거 스웩

답글 달기