본문 바로가기

[협업] 협업을 위한 swagger 설정하기 (feat node.js)

 

 

어가며

스타트업에서 서버 개발자로 일하면서, 불편한 점을 하나씩 해결하고자 노력하고 있습니다. 그중, 하나가 API 명세서 작성에 관한 부분이었습니다. 처음 팀에 합류하고 API 개발을 위해 명세를 찾아봤지만, 제대로 정리된 명세서를 찾아볼 수 없었습니다. 수많은 API가 있었지만, 명세가 없어서 API를 수정하거나, 생성할 때 수많은 시간이 걸렸습니다. 반드시 API 명세서를 제대로 작성해서 업무에 적용시켜야겠다고 생각했습니다. 수많은 삽질을 하면서 swagger를 활용해서 명세를 작성하는 것이 가장 효율적이라고 생각했습니다. node.js를 활용한 swagger 적용 삽질기에 대해 작성하겠습니다. 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

출처 : https://swagger.io/

 

 

 

 

 

Swagger란 무엇인가?

Swagger는 Open API Specification(OAS)를 위한 프레임워크입니다. 간단한 설정으로 프로젝트에서 지정한 URL들을 HTML 화면으로 확인할 수 있게 해주는 프로젝트입니다. Swagger를 활용해서 API 명세를 작성한다면, 보다 쉽게 API 명세를 작성할 수 있습니다. 

 

사실 API 명세를 작성하는 방법에는 여러 가지 방법이 있습니다. 수많은 방법 중 제가 swagger를 선택한 이유에 대해 설명드려보겠습니다. 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

왜 Swaggger를 쓰려고 하는가?

협업을 진행하거나 이미 만들어져 있는 프로젝트에 대해 유지보수를 진행하게 된다면 구축되어 있는 API가 어떤 스펙을 갖고 있는지 알아야 합니다. 스펙을 정리하기 위해 API 문서화 작업을 해야 하는데, 만약 직접 손으로 하게 되는 경우 API를 개발하기 전에 API의 명세와 문서를 작성하고, API가 수정될 때마다 문서도 같이 수정해야 하는 번거로움이 발생합니다. 이런 불편함을 조금이나마 줄여주기 위해 개발된 것이 Swagger 프레임워크입니다. 

 

 

만약 API 명세를 제대로 작성하지 않는다면 어떤 문제가 발생할 수 있을까요?

 

1. API 명세서가 전혀 존재하지 않아서, 코드를 전부 확인하는 과정에서 개발 시간이 너무 오래 걸립니다. 
2. 클라이언트 개발자는 서버 개발자에게 API가 어떻게 동작하는지 물어보면서 개발해야 합니다.
3. 개발할 때는 시간이 적게 걸릴 수 있지만, 유지 보수를 할 때, 시간이 상당히 오래 걸립니다.

 

이 같은 문제를 swagger를 통해 해결할 수 있습니다. 그렇다면, Swagger를 어떻게 적용시킬 수 있을까요?

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Swagger 기본 설정

swagger-jsdoc과 swagger-ui-express 라이브러리를 활용한 개발 방법을 택했습니다.

이 방식은 기존 서버 route에서 바로 코드를 작성할 수 있기 때문에 관리가 쉽고, 접근성이 가장 좋았습니다. 컴포넌트를 별도로 구분해서 관리할 수 있어서 관리하기 쉽고, 사용하기 편리하다고 판단했습니다.

 

swagger를 적용하기 위해서는 npm 모듈을 설치하고, 그 이후에 swagger에 대한 옵션 등을 설정해야 합니다. 그리고 path를 설정해서 자신의 IP에서 확인할 수 있도록 설정해야 합니다. 이들을 하나씩 프로젝트에 적용해보겠습니다. 

 

 

npm mudule 설치

먼저 npm 모듈을 설치해야 합니다. 

 

npm i swagger-jsdoc swagger-ui-express --save-dev

 

swagger는 개발용 모듈이기 때문에 devDependencies에 추가해줘야 합니다. 그래서 --save-dev 옵션을 붙였습니다.

 

설치할 때 swagger-jsdoc이 7.0.0 이상으로 설치가 될 것이기 때문에 이러면 ES6를 반드시 써야 해서, 최신 자바스크립트를 구버전의 자바스크립트로 바꿔주는 babel을 사용하지 않는다면 에러가 발생할 수 있습니다. 그러니 바벨을 사용하지 않는 경우엔 6.0.0 버전으로 다운그레이드 시켜서 설치해야 합니다. 

 

 

 

Options

먼저 Basic Structure에 대해 설정해보겠습니다.

 

프로젝트 구조와 파일 형식은 다 다르겠지만, 저는 modules라는 폴더에 swagger.js 파일을 만들었습니다.

 

const swaggerUi = require('swagger-ui-express');
const swaggereJsdoc = require('swagger-jsdoc');

const options = {
    swaggerDefinition: {
        info: {
            title: 'Test API',
            version: '1.0.0',
            description: 'Test API with express',
        },
        host: 'localhost:3300',
        basePath: '/'
    },
    apis: ['./routes/*.js', './swagger/*']
};

const specs = swaggereJsdoc(options);

module.exports = {
    swaggerUi,
    specs
};

 

모듈을 불러오고, 옵션을 설정한 내용들 뿐이지만, swaggerDefinition는 yaml 형식이나 json 형식을 받습니다. 위는 json을 사용했습니다.

 

info 객체는 title, version, description을 설정했습니다.

 

apis는 내가 설정한 api들을 swagger가 찾을 수 있도록 표시해줍니다. "/routes 파일 아래 js 파일 내에 정의하고 있으며, /swagger 폴더 아래 swagger 설정을 정의하고 있다"를 명시해준 것입니다.

 

이제 기본 설정이 끝났으니 적용시켜보겠습니다.

 

 

app.js, 라우팅 설정

app.js에서 먼저 swagger.js를 불러옵니다.

 

const { swaggerUi, specs } = require('./modules/swagger');

 

그리곤, /api-docs라는 path로 등록시켜줍니다.

 

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

 

이 부분은 모든 path를 정의하기 전, (express에서는 indexRouter 윗부분)에 정의하는 게 좋겠습니다. 여기까지 했다면 설정은 끝났고, 실행해보면 /api-docs에 아래와 같이 뜨는 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

Swagger 정의

API 등록

routes 폴더의 route에서 swagger를 적용해봅시다.

 

swagger를 기존 js 파일에 적용하려면 @swagger를 반드시 붙여줘야 합니다. 그 아래에는 /product가 있는데요. 이는 경로를 나타냅니다. 경로를 설정하는 데에는 몇 가지 방법을 확인할 수 있습니다.

 

/**
 * @swagger
 *  /product:
 *    get:
 *      tags:
 *      - product
 *      description: 모든 제품 조회
 *      produces:
 *      - application/json
 *      parameters:
 *        - in: query
 *          name: category
 *          required: false
 *          schema:
 *            type: integer
 *            description: 카테고리
 *      responses:
 *       200:
 *        description: 제품 조회 성공
 */
router.get('/', getList);

 

경로 설정

1. 아주 기본적으로 /path 만을 적을 수 있습니다.

2. 파라미터를 넣을 수 있습니다. /path/{path_parameter_name} 파라미터를 설정하려면, 위의 코드에서 parameters 아래에 in:path로 설정하면 됩니다.

3. query를 추가할 수도 있습니다. /path/query=q1 이 형태는 어떻게 표시해야 할까요? parameters 아래 보면 in: query를 확인할 수 있습니다.

 

 

메서드

그 아래에는 메서드를 get, post, put, delete 등등을 설정합니다.

 

 

그 밖의 통신 규칙과 설명

그 아래에는 다양한 설정들을 볼 수 있습니다. tags는 해당 API의 태그인데요. /api-docs 페이지에서 그 태그 별로 리스팅 해줍니다.

description는 해당 API를 설명하고, produces는 content-type을 명시합니다. parameters, requestBody, responses 등이 있습니다. 내부 정의 내용은 비슷하니 아래 링크를 보고 공부해야 합니다.

 

 

OpenAPI Specification - Version 3.0.3 | Swagger

OpenAPI Specification Version 3.0.3 The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 RFC2119 RF

swagger.io

 

컴포넌트 정의

routes 폴더와 같은 위치에 swagger 폴더를 만들어서 product.yaml 파일을 만듭니다. yaml은 JSON과 XML과 같이 데이터 직렬화 양식이지만 들여쓰기만을 이용해서 나열하는 아주 간단한 표기법입니다. 이를 활용해서 components를 정의해보겠습니다.

 

# /swagger/product.yml

components:
  schemas:
    Product:
      properties:
        id:
          type: integer
          description: 제품 고유 번호
        title:
          type: string
          description: 제품 이름
        main_image:
          type: string
          description: 제품 메인 이미지
        discount:
          type: integer
          description: 할인 내역

 

path 정의와는 다르게 components로 시작합니다. 이렇게 정의해두고 api의 결과 객체를 설명해줄 수 있습니다. 위의 파일을 적고 나서 가장 중요한 건, api 설정에 추가해줘야 한다는 점입니다.

 

/**
 * @swagger
 *  /product:
 *    get:
 *      // ... 
 *      responses:
 *       200:
 *        description: 제품 조회 성공
          // 아래 schema 추가
 *        schema:
 *          $ref: '#/components/schemas/Product'
 */
 router.get('/', getList);

 

그 후 routes.js에서 위에서 작성한 것에서 schema를 추가합니다. schema: $ref를 추가해서 swagger를 명시합니다.

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

Swagger를 어떻게 적용했는가?

swagger module

const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');

const options = {
    swaggerDefinition: {
        openapi: "3.0.0",
        info: {
            title: 'Poket Univ',
            version: '1.0.0',
            description: 'Poket Univ API with express',
            license: {
              name: "MIT",
              url: "http://localhost:3001/api-docs/"
            },
            contact: {
              name: "epitone",
              url: "https://github.com/epitoneproject",
              email: "epitoneplus@gmail.com"
            },
          },
          servers: [
            {
              url: "http://localhost:3001",
              description: "local Server"
            }
          ],
    },
    apis: ['./routes/*.js', './swagger/*']
};

const specs = swaggerJsdoc(options);

module.exports = {
    swaggerUi,
    specs
};

 

스웨거 모듈을 설정해서, 기본적은 스웨거 동작 방식에 대해 설정했습니다. 이 모듈에서 서버를 나누고, 컴포넌트, 스키마를 참조하는 로직을 설정했습니다. 특히 위의 모듈에서 servers 배열에 API 요청을 하려는 서버를 마음껏 추가할 수 있습니다. 이를 통해 API 요청을 서버를 구분해서 진행할 수 있습니다. 

 

 

 

 

 

 

routes.js

/**
 * @swagger
 *  /qna/specific/{qnaIdx}:
 *    get:
 *      tags:
 *      - Q&A
 *      summary: 'Q&A 상세조회'
 *      description: Q&A 상세조회
 * 
 *      parameters:
 *      - name: token
 *        in: header
 *        description: 헤더에 토큰을 입력하세요
 *        required: true
 *        schema:
 *          type: string
 *        examples:
 *          Sample:
 *            value: example
 *            summary: A sample token
 *        style: simple     
 * 
 *      - name: qnaIdx
 *        in: path
 *        required: true
 *        schema:
 *           type: string
 *        examples:
 *          Sample:
 *            value: 1816
 *            summary: A sample Q&A Idx
 *        style: simple
 *      
 *      responses:
 *       200:
 *        description: Q&A 해당 index 상세 조회 성공
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/getSpecificQna'
 *       403:
 *        description: 헤더의 토큰 값이 만료됐을 때
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/expiredToken'
 *       400:
 *        description: params 값이 없을 때
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/noParams'
 *       404:
 *        description: DB에서 필요한 값을 찾지 못할 때
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/wrongDBIndex'
 * 
 * 
 */

 

MVC 패턴을 사용하는 과정에서, 라우트를 별도로 관리합니다. 이때 routes에 API 목록들이 있는데, 그 위에 swagger 명세를 작성합니다. 이를 통해 API 별로 어떤 데이터를 받아야 하는지를 확인할 수 있습니다. 여기서는 parameter, response 등의 정보를 파악할 수 있습니다.

특히 routes.js에서 header에는 token을, path에는 qnaIdx를 입력받게끔 설정할 수 있습니다. 만약 token과 qnaIdx에 입력에 대한 예시를 보여줄 수 있습니다. examples에서 이에 대한 값을 넣어주면 됩니다. 

 

 

/swagger/qna.yaml

# Q&A API

# getSpecificQna API
components:
  schemas:
    getSpecificQna:
      type: object
      required:
        status
        success
        message
        data
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
        data:
          type: object
          properties:
            question:
              type: object
              properties:
                qnaIdx:
                  type: integer
                title:
                  type: string
                content:
                  type: string
                likeCount:
                  type: integer
                commentCount:
                  type: integer
                createdAt:
                  type: string
                category:
                  type: string
                userIdx:
                  type: integer
                nickname:
                  type: string
                profileImage:
                  type: string
                alarmOn:
                  type: boolean
                initial:
                  type: string
                isLike:
                  type: boolean
                images:
                  type: array
                  items: 
                    type: string
                tags:
                  type: array
                  items:
                    type: object
                    properties:
                      tagIdx:
                        type: integer
                      tagName:
                        type: string
                      initial:
                        type: string
                comments:
                  type: array
                  items:
                    type: object
                    properties:
                      qnaCommentIdx:
                        type: integer
                      content:
                        type: string
                      createdAt:
                        type: string
                      isDeleted:
                        type: boolean
                      userIdx:
                        type: integer
                      nickname:
                        type: string
                      profileImage:
                        type: string
                      initial:
                        type: string
                      isMine:
                        type: boolean
                      isQuestioner:
                        type: boolean
                isMine:
                  type: boolean
            answer:
              type: array
              items:
                type: object
                properties:
                  qnaAnswerIdx:
                    type: integer
                  content:
                    type: string
                  likeCount:
                    type: integer
                  commentCount:
                    type: integer
                  createdAt:
                    type: string
                  selected:
                    type: boolean
                  nickname:
                    type: string
                  profileImage:
                    type: string
                  userIdx:
                    type: integer
                  isMine:
                    type: boolean
                  initial:
                    type: string
                  rank:
                    type: integer
                  images:
                    type: array
                    items:
                      type: string
                  isLike: 
                    type: boolean
                  category: 
                    type: integer
# Q&A API

# createStudyQnaQuestion API
    createStudyQnaQuestion:
      type: object
      required:
        status
        success
        message
        data
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
        data:
          type: object
          properties:
            qnaIdx:
              type: integer

 

swagger 폴더를 만들어서, 스키마를 입력하는 코드를 관리합니다. 위의 파일에서는 Q&A에 대한 기능을 관리하는 API 명세를 작성하고 있으므로 Q&A에 대한 스키마를 작성하는 부분과, 전체적으로 쓰이는 스키마를 구분해서 작성했습니다. 

 

 

 

http 메서드가 post 방식인 경우 multipart/form-data에서 사진 파일 업로드하는 방식을 추가할 수 있습니다. 특히 tagIdx는 array 배열로 요청이 들어오는 것이라서, Add string Item을 누르면 여러 개의 tagIdx를 넣을 수 있습니다. 

 

 

/swagger/etc.yaml

components:
  schemas:
    expiredToken:
      type: object
      required:
        status
        success
        message
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
    noParams:
      type: object
      required:
        status
        success
        message
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
    wrongDBIndex:
      type: object
      required:
        status
        success
        message
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
    noValues:
      type: object
      required:
        status
        success
        message
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string
    errorDB:
      type: object
      required:
        status
        success
        message
      properties:
        status:
          type: integer
        success:
          type: boolean
        message:
          type: string

 

 

특히 스키마를 작성한다면, 이와 같이 요청이 성공한다면, 어떤 데이터를 얻을 수 있는지 미리 파악할 수 있고, 

 

 

 

요청이 실패한다면 어떤 데이터를 얻을 수 있는지도 파악할 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

Swagger를 공부하며 배운 점

저는 항상 노션 또는 github wiki를 통해 명세를 작성했었습니다. 하지만 만약 API가 수정된다면, 노션 또는 위키를 모두 수정해야 하는 불편함이 있었는데, swagger를 적용하면, 코드 내에서 바로 수정할 수 있어서 이 방법이 훨씬 더 간편하다는 것을 배웠습니다. 좋은 개발 문화를 갖춰야 다른 개발자가 프로젝트에 온보딩 할 때 빠르게 프로젝트에 대해 익혀갈 수 있다고 믿습니다. 그 문화를 만들어가기 위해 노력하고 있습니다.

 

그리고 swagger를 적용함으로써 팀에 좋은 영향을 줄 수 있었다고 믿습니다. swagger를 적용함으로써 포스트맨에 들어가는 비용을 줄일 수 있고, 클라이언트 개발자와 효율적인 의사소통이 가능합니다. 그리고 앞으로 팀에 합류하는 서버 개발자와도 효율적인 소통이 가능하며, 앞으로 TDD와 CI&CD를 적용할 때 보다 효과적으로 일할 수 있을 것이라 믿습니다. 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

마치며

앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

참고

 

OpenAPI Design & Documentation Tools | Swagger

Build Build stable, reusable code for your API in almost any language

swagger.io

 

Swagger를 이용해보자! 1편

시작하기 전에 해당 블로그에 작성되는 글은 주인장의 지극히 주관적인 생각이 다수이며, 대부분의 지식은 구글링을 통해 얻고 있기 때문에 옳지않은 정보가 있습니다. 잘못된 부분이나 수정해

real-dongsoo7.tistory.com

 

OpenAPI Specification - Version 3.0.3 | Swagger

OpenAPI Specification Version 3.0.3 The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 RFC2119 RF

swagger.io

 

Node.js + Swagger, 어렵지 않게 사용하기

안녕하세요 ! 오늘도 node 서버와 관련된 게시글을 작성하려고 합니다 〰️ 오늘은 swagger에 사용법에 대해 알아보겠습니다 ~.~ 그동안,,, API 명세서 작성하고 수정하고 ,,, 힘드셨죠 🤣 ***************

gngsn.tistory.com

 

[TIL] - Express 에 Swagger 사용하기

Swagger가 뭐야? swagger.png 백엔드 프로그램과 프론트 프로그램 사이에서 정확히 어떤 방식으로 데ㅣ터를 구조 받을 지에 대한 명세서인 API를 관리할 수 있는 도구이다. 지금까지는 엑셀파일이나

velog.io