Contents

Swagger를 이용한 백엔드의 효과적인 API명세 전달

사진출처: unsplash
 
 

백엔드가 효과적인 명세 전달 고민

 

Failure
그동안 진행했던 프로젝트에서 제대로 된 API 명세를 적시에 프론트측에 전달하지 못했고,
때문에 최종 출시를 앞두고 서로간의 코드 오류를 잡느라 시간적 비용이 꽤나 낭비됐다. 간단하게는 Request Body 값의 TYPE과 관련된 소통 혼선으로 겪었던 헤프닝도 더러 있었다.

 

어떻게 하면 좋을까?
 

우선은 백엔드의 입장에서 생각해본다.

  • 명세는 코드가 만들어지면서 늘 수정될 수 있다.
  • 때문에 명세는 코드와 함께 작업되어야 하고, API 테스트를 위해 즉각적으로 수정되어야 한다.
  • 같은 백엔드 팀의 경우에는 직접 코드를 보기에 명세의 부족함을 보완할 수는 있다.
  • 반면 프론트 팀으로 넘어갈 때는 이야기가 다르다. 최대한 문서만으로 전달이 되었을 때, 그리고 전달이 가능할 때, 불필요한 소통이 사라진다.

지금까지는 그때그때 백엔드와 프론트엔드가 코딩을 하며 API를 맞춰나갈때마다 일대일로 물어보는 방식이었다.  

불필요한 대화 시간과 에너지가 크게 소모되었으리라.
이런 비효율적인 방법이 아닌, 좀 더 생산적이고 효과적인 방법을 도모하고 싶었다.
 

그간 사용해왔던 API 툴은 Postman API Platform | Sign Up for Free이었다.
불편함을 느낀건 명세를 전달하기 위한 문서작성을 IDE나 텍스트편집기가 아닌 웹 브라우저나 Postman의 자체 클라이언트 앱에서 진행을 해야했다는 점이 가장 컸다.1
 

또한 문서가 완비되기 전의 상황에서도 프론트 측으로 공유를 하고 싶은데 이 부분에 있어 Postman은 유료 플랜, 즉 비용이 발생하였다.2
 

때문에 새로운 툴을 찾아보다 **Swagger**를 알게되었다.
 

Postman과 Swagger의 차이

간단하게 기술하자면,

  • open source 여부
  • 이로 인한 무료 커버리지 및 커스터마이징 가능 여부 정도로 볼 수 있겠다.

Swagger가 open source이다.

이전에는 Swagger로부터 비롯되었다하여 현재의 국제표준인 openAPI도 Swagger의 특징으로 볼 수 있는데, 현재 시점에서는 그다지 차이가 없는 듯 보인다.


 

Postman vs Swagger 사용 후기

Postman

장점
  • 초보자도 이해하기 쉬운 UI UX
  • 편리한 API 테스트
  • 잘 만들어진 desktop API Client app

 

단점
  • 팀원간 명세 전달시 비용 발생
  • 명세 작성시, 코드 개발자는 IDE 또는 편집기에서 벗어나 작업 진행

 

Swagger

Swagger의 경우 장단점을 언급하기 전, 먼저 사용방법에 대한 간략한 설명을 한다.
 

Swagger API 명세 사용법
  1. SwaggerHub 이용
    API 개수에 제한이 있지만 무료로도 가능하다.
  2. SwaggerUI 이용
    http-server를 사용하여 열람 및 API 테스트 가능
  3. SwaggerUI를 프로젝트 코드에 import하여 사용[^
    NodeJS의 경우 보통 route단에서 주석+데코레이터 형태로 사용할 수 있다.

 

사용방법
  1. SwaggerHub 이용
    API 개수에 제한이 있지만 무료로도 가능하다.
  2. SwaggerUI 이용
    http-server를 사용하여 열람 및 API 테스트 가능
  3. SwaggerUI를 프로젝트 코드에 import하여 사용3
    NodeJS의 경우 보통 route단에서 주석+데코레이터 형태로 사용할 수 있다.
장점
  • 2, 3번을 사용할 경우 코드 개발자는 IDE 또는 편집기 내에서 API 명세를 그대로 작성할 수 있다.
  • 또한 2, 3번의 경우 비용이 발생하지 않는다.
  • 3번의 경우 git으로 관리가 가능하기에 팀원간 명세 전달시 편리하다.
  • API 명세 문서임과 동시에 API 통신 TEST 까지 가능하다.
  • 오픈소스이기에 명세 전달시 주어지는 UI 역시 수정이 가능하다. 팀의 성격이나 전달자의 입맛에 따라 바꿀 수 있다.  
단점
  • 어려운건 아니지만, 어쨌든 Postman에 비해 러닝커브가 있다.
  • 일단 문법이나 코드형식을 익혀야 한다.

 

Swagger 적용 이야기

과도기

문법도 맛볼겸 사용방법 3번을 우선 도입했다.
모든 준비와 테스트를 끝내고 막상 전달을 하려고 보니,
프론트엔드 팀이 이를 보려면 백엔드의 깃을 클론하고 DBMS를 비롯한 모든 세팅을 해야만 서버가 돌아가기에 정상작동을 한다.
몹시 불편하지 싶다.
 

1번을 사용해볼까는데, 링크전달이라… 4
비공개 프로젝트로 진행하게될 사안도 염두에 두었다.
(물론 불가능한건 아니지만, 비용문제를 간과할 수 없었다.)
 

어떻게든 1번만은 피하고 싶었던지라 이번에는 다시 2번으로 확정짓고 세팅을 했다.
 

그런데 진행간에 문득 백엔드 팀원간의 명세 교류는 어떡하지?란 생각이 들었다.
(이걸 간과한 이유는 현재 진행중인 프로젝트의 백엔드가 나 혼자이기 때문이었다.)
 

한참을 고민하다
2번과 3번의 하이브리드 방법을 모색해보았다.
만약 2번과 3번이 참조하는 swagger.yaml을 하나로 지정을 한다면?
 

간단한 테스트를 통과시키고 이 방법으로 진행하기로 했다.  

SwaggerUI + 프로젝트 코드 주입 방식

미리 언급하지만 2가지 방법을 혼용한 내용임으로,
처음 Swagger를 사용해보기 위해 이 글을 참조한다면 몹시 혼란스러울 수 있다.
따라서 참조된 다른 문서에서 하나씩 실행해보며 작동을 익힌 후 참고하길 권장한다.

 

별도의 폴더를 route단에 주석으로 넣는 방식은 아래 예시처럼 작성하게 되는데 정말 불편하다.5
 

// routes/users.js

/**
 * @swagger
 * /users:
 *   get:
 *     summary: Retrieve a list of JSONPlaceholder users
 *     description: Retrieve a list of users from JSONPlaceholder. Can be used to populate a list of fake users when prototyping or testing an API.
*/
router.get('/', function(req, res) {
  //...
});

 

이런식으로 데코레이터를 비롯한 주석처리로써 swagger-ui-jsdoc가 코드를 읽을 수 있게 처리하는건데, API별 명세는 한 두개가 아닐뿐더러 자세하게 작성하기 시작하면 수십줄은 훌쩍 넘어간다.
 

즉, 자칫 route파일의 가독성이 몹시 불편하게 될 수 있어 보였다.  

물론 해당 주석부분을 collapse all 처리해도 되지만 그때그때 expand하고 다시 collapse 하는게 역시나 불편하긴 마찬가지다.
 

때문에,
프로젝트 코드의 swagger.js파일의 세팅을 별도의 폴더로 지정하고 SwaggerUI를 프론트단에서도 쓸 수 있도록 js파일이 아닌 yaml파일로 작성하기로 하고, 명세 종류가 많아짐에 따라 가독성 저하로 인한 파일 분기를 모색했다.6
 

설치

SwaggerUI 세팅

SwaggerUI는 세팅이 굉장히 쉽다.
 

swagger-api/swagger-ui 코드를 클론하고 dist폴더만 남긴채 모두 지워준다.(나머지는 모두 참고문서)
 

현재시점인 Swagger UI v4.17.1의 dist폴더를 살펴보면 index.html에서,
내가 사용하고자 하는 URL을 지정하는 위치는 다음과 같다.
 
/images/Pasted%20image%2020230308101401.png
 

// swagger-initializer.js

window.onload = function () {  
  //<editor-fold desc="Changeable Configuration Block">  
  
  // the following lines will be replaced by docker/configurator, when it runs in a docker-container  window.ui = SwaggerUIBundle({  
    url: 'http://localhost:8080/index.yaml', // <= 바로 이부분을 고쳐준다. 
    dom_id: '#swagger-ui',  
    deepLinking: true,  
    presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],  
    plugins: [SwaggerUIBundle.plugins.DownloadUrl],  
    layout: 'StandaloneLayout',  
  });  
  
  //</editor-fold>  
};

 

저 부분이 앞으로 사용하게 될 SwaggerUI의 html파일이 가르키게 될 나의 Swagger 메인 문서 위치이다.  

나는 dist폴더의 파일들을 적용할 프로젝트 폴더 안 api-docs 라는 폴더를 만들고 그 안에 배치하였다. 그리고 api-docs폴더의 tree는 다음과 같다.

 

.
├── README.md                <- 이건 후술할 git submodule때문에 생성
├── _build
│   └── index.yaml
├── favicon-16x16.png
├── favicon-32x32.png
├── index.css
├── index.html
├── oauth2-redirect.html
├── swagger                  <- 이 폴더안에 index.yaml을 만들어서 세팅한다. 
│   ├── index.yaml
│   └── user                 <- 내용이 방대함으로 성격에 따라 폴더와 파일들로 분기했다. 
│       ├── signIn.yaml
│       └── signUp.yaml
├── swagger-initializer.js
├── swagger-ui-bundle.js
├── swagger-ui-bundle.js.map
├── swagger-ui-es-bundle-core.js
├── swagger-ui-es-bundle-core.js.map
├── swagger-ui-es-bundle.js
├── swagger-ui-es-bundle.js.map
├── swagger-ui-standalone-preset.js
├── swagger-ui-standalone-preset.js.map
├── swagger-ui.css
├── swagger-ui.css.map
├── swagger-ui.js
└── swagger-ui.js.map

 

tree 확인!! (click)
나는 project 내 api-docs라는 폴더를 만들고 그 안에 SwaggerUI의 dist 폴더안 파일들을 위치시켰다.
그리고 swagger라는 폴더를 따로이 생성한 뒤 그 안에 index.yaml파일을 세팅하는데 그 이유는 앞서 언급한 파일 분기 때문이다.

 

그리고 다음과 같이 index.yaml을 작성해준다.
 

# api-docs/swagger/index.yaml

openapi: 3.0.0  
servers:  
  - description: project_review localhost API document  
    url: http://localhost:8000  
info:  
  version: "1.0.0"  
  title: project_review-API  
  description: The API for project_review  
  contact:  
    name: Inchan Song  
    url: https://github.com/inchanS  
    email: [email protected]  
paths:  
  /users/signup:  
    $ref: "./user/signUp.yaml"    #코드가 길어지면서 지저분해짐에 따라 분기하고 참조했다.
  /users/signin:  
    $ref: "./user/signIn.yaml"

 

swagger 명세 작성

index 내용중 경로와 사용처 확인!!
api-docs/swagger/index.yaml 파일은 http-server에서 돌아가는 SwaggerUI가 사용하는 파일이다. 그리고 swagger.ts에 작성된 info는 node서버에서 돌아가는 SwaggerUI가 사용하는 파일이다.
 

API 명세의 자세한 내용은 다음과 같은 형식으로 작성하였다.
 

API 명세 yaml 파일 보기 (클릭)
# api-docs/swagger/user/signin.yaml

post:  
  tags:  
    - User  
  summary: 회원 로그인  
  description: 회원 로그인 관련 API  
  produces:  
    - application/json  
  requestBody:  
    description: 회원 로그인시 필요한 사용자 정보 객체  
    required: true  
    content:  
      application/json:  
        schema:  
          $ref: '#/components/schemas/User'  
  responses:  
    200:  
      description: successful operation  
      content:  
        apllication/json:  
          schema:  
            $ref: '#/components/schemas/Success'  
    500:  
      description: Internal server error  
      content:  
          apllication/json:  
            schema:  
              $ref: '#/components/schemas/Error'  
components:  
  schemas:  
    User:  
      type: object  
      properties:  
        email:  
          type: string  
          description: The email of the user  
          example: '[email protected]'  
        password:  
          type: string  
          description: The password of the user  
          example: 'Abcd@1234'  
    Success:  
      type: object  
      properties:  
        message:  
          type: string  
          description: 로그인 성공 메세지  
          example: 'SIGNIN_SUCCESS'  
        result:  
          type: object  
          description: 로그인 성공시 토큰값  
          properties:  
            token:  
              type: string  
              description: 토큰값  
              example: "token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTYsImlhdCI6MTY3ODEyOTY0OX0.eyNOFEZt5xPSakzy4FyGqub9heSZA5FtOZGtWyTI4vg'"  
    Error:  
      type: object  
      properties:  
        message:  
          type: string  
          description: 로그인 실패시 메세지  
          example: '(THIS_EMAIL)_IS_NOT_FOUND'

 


 

Tip

일반적인 $ref <path>는 대부분 코드가 잘 작동하지만,
paths의 값으로 `$ref 를 넣으면 swagger가 처리를 못하는 것 같다.
때문에 다음과 같은 추가 작업이 필요하다.
 

"scripts": {  
  "test": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles --forceExit",  
  "build": "npx tsc",  
  "api-docs": "swagger-cli bundle api-docs/swagger/index.yaml --outfile api-docs/_build/index.yaml --type yaml",  
  "predev": "npm run api-docs",  
  "start": "cross-env NODE_ENV=production node dist/main.js",  
  "dev": "cross-env NODE_ENV=develop concurrently \"npx tsc --watch\" \"nodemon -q dist/main.js\"",

위 json파일에서 api-docs, predev script 명령을 살펴본다.
전자의 명령은 분기한 swagger의 yaml 파일들을 하나의 파일로 모아서 새로 생성해주는 역할을 한다.
그리고 후자의 명령은 이를 dev명령이 실행되기전 사전작업으로 실행되도록 해주는 명령이다.
이로써 node서버를 돌릴때마다 저 명령이 실행되며 하나의 index.yaml파일로 만들어주며 이로 인해 paths: $ref <path>의 오류를 잡을 수 있게 된다.1


 

그리고 이 파일을 열어볼 수 있는 방법은 다음과 같다.  

SwaggerUI 실행하기

Node.js를 사용하는 http-server를 설치하고, http-server를 사용하여 API를 테스트할 수 있다.
다음 순서를 따라 실행할 수 있다.
 

  1. 먼저 http-server를 설치.
    $ npm install http-server -g  
    

 

  1. 설치가 완료되면, 다음과 같은 명령어로 API 명세서 확인 및 테스트 페이지를 호출 가능하도록 서버를 실행시킬 수 있다.
    // 열고자 하는 파일이 있는 directory에서  
    
    $ http-server --cors 
    
    // 입력하면 사용할 수 있는 주소가 나온다. "-p 8000" 등으로 포트번호 지정도 가능하다.  
    // http-server -p 8000 --cors  
    

 

  1. 페이지 호출은 2가지 방법으로 가능하다.
    http://localhost:8080으로 접속하면 다음과 같은 화면을 볼 수 있다.
    또는 폴더내 index.html을 그냥 브라우저로 열어도 동일한 화면을 볼 수 있다.
    /images/webscreenshot.png

 

Swagger.js파일 적용

이제 방금 세팅한 SwaggerUI에서 사용할 yaml 명세를 Node 서버 안에서도 돌아갈 수 있도록 세팅을 한다.
 

npm 설치

2개의 npm package를 필요하다.

  • swagger-jsdoc(js에서 swagger 문서를 읽고 쓸수 있도록 해준다.)
  • swagger-ui-express(express 서버에서 SwaggerUI가 돌아갈 수 있게 해준다.)
     
// javascript 사용시,
npm i -D swagger-jsdoc swagger-ui-express

// typescript 사용시 아래 패키지도 "추가적으로" 설치해준다.  
npm i -D @types/swagger-jsdoc @types/swagger-ui-express

 

swagger.js 세팅

현재 프로젝트의 폴더트리는 대략 다음과 같다.
 

/images/Pasted%20image%2020230308104407.png
 

앞에서 모두 처리하였기에 여기서는 2가지만 처리하면 된다.
1. swagger.ts(js) 생성7
2. app.ts(js) 코드 추가8

swagger.ts(또는 js)를 적당한 위치에 생성하고 다음과 같이 코드를 작성한다.
 

import swaggerJsdoc from 'swagger-jsdoc';  
  
const options = {  
  swaggerDefinition: {  
    openapi: '3.0.0',  // 사용하는 버전을 적어준다. 버전별 문법이 조금씩 상이하다.  
    info: {  
      title: 'Project_review',  // 프로젝트명과 부연정보 기입
      version: '1.0.0',  
      description: 'project_review API with express',  
      license: {  
        name: 'MIT',  
        url: 'http://localhost:8000/api-docs/',  
      },  
      contact: {  
        name: 'Inchan Song',  
        url: 'https://github.com/inchanS',  
        email: '[email protected]',  
      },  
    },  
    servers: [  
      {  
        url: 'http://localhost:8000', // 이건 node서버가 돌아가는 주소!!  
        description: 'local Server',  
      },  
    ],  
  },  
  apis: ['api-docs/index.yaml'],  
};  
  
const specs = swaggerJsdoc(options);  
  
export { specs };

 

Tip

위 코드 중 apis의 값으로 들어가는 게 바로 swaggerUI가 사용하게 될 파일이다.
만약 component파일을 따로이 분기해서 작성했다면 그 폴더를 이어서 작성해주면 된다.
예시,

apis: ['api-docs/*', 'api-docs/swagger/*],  

이렇게 하면 첫번째 배열의 파일을 읽고, 두번째 배열의 파일을 참조하게 된다.


 

Error - No operations defined in spec! (click)

swagger.html에서

No operations defined in spec!

이렇게만 뜨고 API 내역이 나오지 않는다.

apis의 경로를 상대경로에서 절대경로로 해결할 수 있다!

참고문서


 

이제 app.ts를 수정한다.
 

import { specs } from './utils/swagger';  // 방금 생성한 swagger.ts파일
import swaggerUi from 'swagger-ui-express'; // npm으로부터 import

// ... 코드 중략

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));  
// '/api-docs'가 라우터 주소이다. 

app.use(router);

// ... 코드 생략

 

node 서버에서의 SwaggerUI 접속은 ’localhost:8000/api-docs’가 된다.  

url 주소 설정시 주의 (click)
위 코드 중 swaggerDefinition.server에서 url은 앞서 설치한 SwaggerUI의 url과 다르다.
이건 node 서버를 돌리는 주소!!
즉 나중에 배포하게 되면 배포주소로 바뀌어야 한다.

 

거의 다 끝났다.
이제 프론트엔드와 백엔드에게 각각 어떻게 전달을 할 것인가?

  • 백엔드의 경우에는 팀 깃 레포 공유로 인해 PR을 할때마다 추가되니 문제될 것이 없다.
  • 프론트의 경우에는?? 따로이 레포를 줘야 하나?

 

git submodule 로 해결

api-docs 폴더를 그대로 git submodule로 새로 만든 레포지토리에 연결을 한다.
나는 “진행중인 프로젝트명-API"라는 이름으로 생성하였고,
다음과 같이 진행하였다.

 

# submodule 진행할 디렉토리로 이동후, 
$ git init 
$ git remote add origin <새로만든 레포지토리> 

# 그리고 project root폴더로 이동후,
$ git submodule add -b main <새로만든 레포지토리> <submodule 진행할 디렉토리>

# 예시...
$ git submodule add -b main https://github.com/inchanS/project-review-API-docs.git api-docs

 

그리고

# project root폴더의 위치에서 

$ git submodule status

# 명령하면 연결된 submodule의 내역이 나온다. 

 

이제 코딩을 진행하며 백엔드 코드는 기존과 같이 커밋과 푸쉬를 자유롭게 진행하고,
 

프론트엔드 측으로 전달할만큼의 API 명세가 update 됐을 때에는,
따로이 submodule 폴더만 해당 레포지토리에 commit, push를 진행하면 된다.
 

그리고 실수로 올라간 push로 인해 불필요한 알림을 최소화 할 수 있도록,
버전 release 태그를 활용하여 여기에만 slack에 webhook 될 수 있도록 세팅을 한다.

그러면 다음과 같이 github의 release로 필터링된 멋진 알림이 가게 된다.
 
/images/Pasted%20image%2020230308125411.png  

아쉬운 점

http-servernode서버 없이도 명세만 볼수 있게끔 완전한 HTML 파일만 추출 할 수 없나 고민중이다.
어찌됐든간에 html파일을 바로 열어볼 수 있는 것과, http-server 명령어를 입력하고 열어볼 수 있는건 사용자 경험에 있어 하늘과 땅차이니까 말이다.
현재 SwaggerUI의 코드가 워낙 복잡(?)해서 건드릴 엄두가 나질 않지만, 차후 시도해볼만하지 않나 싶다.


  1. 대량으로 작성하다보면 셀 이동이나 유사코드 붙여넣기와 같은 작업을 비롯하여 키보드에서 손을 떠나게 만드는게 스트레스였다. ↩︎

  2. Postman API Platform | plans & pricing 이 글을 작성하는 현재시점에서 1인당 매월 $12라는 꽤나 높은 비용이다.(현재 환율 1,300원을 반영해보면 대략 월 15,600원 정도 나온다.) ↩︎

  3. [협업] 협업을 위한 swagger 설정하기 (feat node.js) 참고   ↩︎

  4. 간편하게 사용하거나 최종 배포 시점에 작성한다면 되려 더 편할지도 모르겠지만 프로젝트 과정간 계속해서 사용하기에는 불편해보였다. 또한 링크의 공개성과 소멸시효에 따른 우려때문에… ↩︎

  5. 첨부코드 참고문서 How to Document an Express API with Swagger UI and JSDoc - DEV Community ↩︎

  6. 참고문서 How to split a large OpenAPI document into multiple files - David Garcia ↩︎

  7. 파일명은 아무거나 해도 상관없다. app.ts에서 import할때 이름만 같으면 되기에 아무거나.ts라고 해도 작동한다. ↩︎

  8. 만약 main.ts에서 app을 분기시키지 않았다면 해당 파일에서 코드를 추가한다.
      ↩︎