Node.js (TS) 프로젝트에 swagger 적용하기 (Feat. 파일 분리)

김혜지·2020년 11월 21일
6

Content

들어가며

API 명세를 보통 어디에, 어떻게 작성하는가? 나는 다음의 세 가지의 조건을 모두 충족해야 한다고 생각한다.
1. 프론트엔드와 백엔드가 모두 접근 가능해야 한다.
2. 수정이 용이해야 한다.
3. 문서와 코드는 항상 동일해야 한다.

이 조건들에 따라 나는 팀원과 같이 사용하는 Notion과 같은 공용 문서 도구, 또는 모든 사람들이 접근가능하며 개발 시에 자주 들어가는 Github의 Wiki에 API 명세를 작성하곤 했다. 하지만 위 두 가지 방식은 3번째 조건을 지키기 어려웠다.
여기서 Swagger가 등장한다. 개발자가 설정한 URL로 (자동 배포를 한 경우) 언제나 접근이 가능하며 API 명세를 코드로 나타내어 수정이 용이하면서도 명세와 코드를 IDE 상에서 쉽게 비교하여 통일시킬 수 있다.

프로젝트에 swagger를 적용시키는 과정만 보고 싶다면 적용 과정로 바로 이동하면 된다. 나머지는 뭐 그냥 주저리주저리..

Swagger

Swagger는 OpenAPI 정의를 사용하여 API를 코드로 작성하고, Swagger UI를 사용하여 프로젝트에서 지정한 URL에 문서를 보여줄 수 있게 해주는 도구이다. Swagger UI가 제공해준 페이지를 통해 서버로 요청을 보낼 수 도 있고 응답을 확인할 수 도 있다.
Swagger에 대한 정확한 정보는 공식 문서를 읽어보길 바란다. 그 중에서도 Basic StructureComponents Section 문서를 읽으면 어떻게 작성해야 하는지 금방 파악할 수 있을 것이다.

적용 과정

해당 프로젝트는 TypeScript, Node.js, express를 사용한다.

0. 프로젝트 파일 구조

1. Install

필요한 패키지들을 먼저 설치해준다.

npm install swagger-cli swagger-ui-express yamljs
npm install -D @types/swagger-ui-express @types/yamljs

swagger-cli 쪼개놓은 yaml 파일을 합칠 때 사용
swagger-ui-express 작성해둔 API 명세를 UI로 보여준다.
yamljs TS 파일로 yaml 파일을 읽어오기 위해 사용

⚠️ swagger를 개발 시에만 사용하고 싶은 경우
API 문서가 개발 시에만 필요하다면 모두 devDependencies 로 설치해도 되겠지만, 그렇게 하면 린트 에러가 발생한다.
우리 팀은 개발, 배포 시에도 API 문서가 필요해서 dependencies로 진행했다.

⚠️ 왜 yamljs 를 선택했을까?
사실 JS에서 yaml 파일을 읽어오기 위해 가장 많이 사용하는 라이브러리는 js-yaml이다. 그러나 TS를 막 도입한 우리 프로젝트에서, js-yaml를 사용해 읽어온 yaml을 swagger 설정 코드에 넣으려고 하니 타입 불일치 문제가 발생했다. yamljs 라이브러리의 경우, 파일을 읽어왔을 때 any 타입을 반환하여 문제가 없어 선택하게 됐다.

2. openapi 설정과 UI의 URL 지정

swagger는 openapi 정의를 제공하는데, 하나의 버전을 선택해서 그 버전의 문법을 따라 명세를 작성하면 된다. 따라서 가장 먼저 openapi 설정부터 작성해준다.

openapi.yaml

openapi: '3.0.0'
info:
  version: 1.0.0
  title: Slack Web API docs
  description: Slack Web TEAM A의 API 문서입니다
  license:
    name: MIT
servers:
  - url: http://localhost:3000/

그리고 만들어준 이 yaml 파일을 app.ts에 읽어와서 swagger-ui와 연결해준다.

app.ts

import swaggerUi from 'swagger-ui-express'
import YAML from 'yamljs'

...

const swaggerSpec = YAML.load(path.join(__dirname, '../build/swagger.yaml'))

...

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))

/build/swagger.yaml 파일은, openapi.yaml을 포함해서 API 명세를 작성한 모든 yaml 파일을 하나로 합쳐준 파일이다. 이 작업을 바로 swagger-cli가 해주는데, 편하게 하기 위해 package.json에 스크립트로 등록해주자.

package.json

"scripts": {
    "start": "tsc && node build/app.js",
    "api-docs": "swagger-cli bundle ./src/swagger/openapi.yaml --outfile build/swagger.yaml --type yaml",
    "predev": "npm run api-docs",
    "dev": "nodemon --exec ts-node src/app.ts"
  },

startdev는 원래 있는 스크립트이다. api-docs가 바로 ./src/swagger/openapi.yaml 부터 읽어서 관련된 모든 파일을 통합해 build/swagger.yaml로 만들라는 스크립트이다.
또한 npm run dev 명령어를 실행할 때 항상 swagger가 최신으로 유지되도록 predev 스크립트를 추가했다.

이제 npm run dev을 실행시켜보면 다음과 같은 화면을 볼 수 있다.

3. API 명세 작성

위에서 만들어준 openapi.yaml 파일에 API 명세를 작성해보자.

openapi.yaml

openapi: 3.0.0
info:
  version: 1.0.0
  title: Slack Web API docs
  description: Slack Web TEAM A의 API 문서입니다
  license:
    name: MIT
servers:
  - url: 'http://localhost:3000/'
paths:
  /user:
    get:
      summary: Returns user list
      responses:
        '400':
          $ref: '#/components/responses/BadRequest'
components:
  parameters: null
  schemas:
    User:
      type: object
      required:
        - _id
        - name
      properties:
        _id:
          type: number
          description: id
        name:
          type: string
          description: 유저 이름
    Error:
      type: object
      properties:
        success:
          type: boolean
        message:
          type: string
  responses:
    BadRequest:
      description: 잘못된 요청
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: 잘못된 요청
    InternalServerError:
      description: 서버 에러
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: 서버 내부 오류

간단한 예시로 GET /user API 에 대한 명세를 작성해주고, 반복적으로 사용될 스키마, 응답 또한 작성해주었다. 파일을 저장하고 (아까 npm run dev를 실행시킨 상태에서 다른 터미널을 켜서) npm run api-docs 를 실행한 다음 새로고침을 눌러보면 다음과 같은 화면을 볼 수 있다.

4. 파일 분리

간단한 예시로 paths 만 분리시켜 보겠다.

openapi.yaml

Before:

paths:
  /user:
    get:
      summary: Returns user list
      responses:
        '400':
          $ref: '#/components/responses/BadRequest'

After:

paths:
  $ref: './paths/_index.yaml'

이제 /paths/_index.yaml` 파일을 생성해주고 코드를 옮겨주자.

/paths/_index.yaml

/user:
  get:
    summary: Returns user list
    responses:
      400:
        $ref: '../openapi.yaml#/components/responses/BadRequest'

변경된 부분이 보이는가? openapi.yaml 에 paths가 있을 때는 $ref 경로가 #으로 시작되었지만, 분리된 /paths/_index.yaml 파일에서는 ../openapi.yaml# 으로 시작되는 것을 확인할 수 있다. 다른 paths 이나 parameter, schemas, responses 모두 동일한 방식으로 분리해 나갈 수 있다.

이제 다시 npm run api-docs를 실행시켜 주면 3번의 실행 결과와 완벽하게 동일한 결과가 나올 것이다.

왜 이렇게 했을까?

우리 팀은 Node.js 로 백엔드를 개발하고 있는데, Node.js에 Swagger를 적용시키는데는 많은 방법이 있다. 가장 보편적으로는 swagger-jsdoc 라이브러리를 사용하여 각각의 API 코드 상단에 직접 명세를 작성할 수 도 있고, 하나의 yaml 또는 json 파일에 모든 API 명세를 때려박을 수 도 있다. 문제는 두 가지 방법 모두 단점이 보였다는 것이다.

  1. swagger-jsdoc 사용해서 API 코드 상단에 명세 작성

위 코드는 단 하나의 API 에 대한 명세이다. 심지어 전부가 아닌 1/2 정도이다. Swagger는 재사용가능한 파라미터, 응답, 스키마를 분리하여 참조하는 형식으로 코드의 길이를 줄일 수 있는데 (내가 못찾은 것 같긴 하지만) swagger-jsdoc는 다른 파일을 참조할 수 없었다. 따라서 보통 하나의 컨트롤러 파일에 CRUD 네 가지의 미들웨어가 존재한다고 치면, 위 사진에서 보이는 명세의 최소 5배는 추가될 것이다. 당연히 파일이 더럽겠죠?

  1. 하나의 yaml 또는 json 파일 사용

하나의 파일에 모든 API 명세, 파라미터, 스키마, 응답을 모두 적는 방법이다. yaml이나 json 파일 모두 들여쓰기가 중요한데, 하나의 파일에 수많은 명세를 작성하다보면 들여쓰기로 인한 오류는 물론, 내가 보고자 하는 API 명세를 찾기는 굉장히 어렵고 눈 아플 것이다.

그래서 우리는
yaml을 여러 개로 분리하여 명세와 개발을 진행하고, API 명세를 UI로 확인하고자 할 때는 이를 하나의 yaml 파일로 합치는 방식을 채택했다. 이 방법도 물론 단점이 있다. 개발 시에는 nodemon을 사용하는데, yaml파일이 변경되면 새로운 하나의 yaml을 생성해야 하는 것을 nodemon이 알 수 없다. 따라서 API 명세에 대한 코드가 변경되면 직접 파일을 생성해줘야 한다.

Reference

profile
Developer ( Migrating from https://hyex.github.io/ )

1개의 댓글

comment-user-thumbnail
2021년 7월 5일

잘 읽었습니다ㅎㅎ

답글 달기