파크로그
article thumbnail

Swagger?

프론트엔드-백엔드가 같이 프로젝트를 하면 API 명세를 통해 의사소통을 하게됩니다.

이 때 API 명세는 사람이 직접 적는 문서 형식으로 Notion, Github Wiki 등을 활용할 수는 있지만 몇가지 불편한 점이 있습니다.

 

1. 변경을 추적하기 어렵다는 점

2. 변경이 될 때마다 프론트엔드 에게 인지를 시켜줘야 한다는 점

3. 혹시 모를 오탈자로 고생할 일이 적어짐

 

세 가지 모두 프로젝트를 하면서 겪었던 문제였는데요, Swagger 를 통해 문서화를 자동화하면 이를 해결해볼 수 있습니다.

 

🚀 설치

npm install -D swagger-jsdoc swagger-ui-express

- -D 옵션으로 devDependency 에 설치하였습니다.

- swagger-ui-expressswagger-ui 를 express 에서 더 편하게 사용할 수 있게 도와주는 라이브러리입니다.

- swagger-jsdoc 라이브러리는 주석이 달린 소스 코드를 읽고 OpenAPI(Swagger) 사양을 생성합니다.

 

🔥 적용

간단한 todos 조회와, 생성, 삭제 API 를 만들었습니다. 저장소



const express = require('express');

const PORT = 3000;

const app = express();
app.use(express.json()); // body-parser 내장된 것 사용

let fakeTodos = [];
let lastTodoId = 0;

app.get('/', (req, res) => {
  res.send({ data: 'home' });
});

app.get('/todos', (req, res) => {
  res.send({ data: fakeTodos });
});

app.post('/todo', (req, res) => {
  const { subject } = req.body;
  const newTodoId = lastTodoId + 1;
  lastTodoId += 1;
  const newTodo = { id: newTodoId, subject };
  fakeTodos = [...fakeTodos, newTodo];
  res.sendStatus(201);
});

app.delete('/todo/:id', (req, res) => {
  const { id } = req.params;
  fakeTodos = fakeTodos.filter((todo) => todo.id !== Number(id));
  res.sendStatus(204);
});

app.listen(PORT, () => {
  console.log(`✅ Server listening on : ${PORT}`);
});

 

위 API 들에 Swagger 를 적용해보겠습니다.

 

우선 프로젝트의 폴더구조는 다음과 같습니다.

├── package-lock.json
├── package.json
└── src
    └── app.js

src 내부에 swagger.js 와 swagger 의 schema 들을 모아둘 swagger 폴더를 만들겠습니다.

├── package-lock.json
├── package.json
└── src
    ├── app.js
    ├── swagger
    └── swagger.js

swagger.js 에서는 설치한 swagger-ui-express 와 swagger-jsdoc 을 불러와 UI 와 specs 를 만들겠습니다.

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

const options = {
  swaggerDefinition: {
    openapi: '3.0.0',
    info: {
      title: 'Todo API',
      version: '1.0.0',
      description: 'Todo API with express',
    }
  },
  apis: ['./src/app.js', './src/swagger/*'],
};

const specs = swaggereJsdoc(options);

module.exports = {
  swaggerUi,
  specs,
};

swaggerDefinition

- OpenAPI 에서 설명하고있는 definition 에 대해서 옵션을 설정해줍니다. 해당 옵션의 key 값은 swaggerDefinition 으로 해도 되고, definition 으로 해도 됩니다. (swagger-jsdoc 의 getting started)

openapi

- swagger 2.0 을 사용할 때에는 openapi : '3.0.0' 대신 swagger : '2.0' 을 required 로 사용해야 했습니다. Schema/Swagger Object Fixed Fields 참고

- 3.x 으로 변경되면서 openapi 를 required 로 적어주면 됩니다. (Schema/OpenAPI Object Fixed Fields) 3.x.x 로 적어주면 되는데 이 예제에서는 3.0.0 을 사용하겠습니다.

 

info

- swagger ui 에 보여질 정보를 입력합니다. info object

 

info 에 입력된 정보들은 해당 위치에 보여집니다

만약 server 의 정보를 입력하고 싶다면 servers 에 정보를 입력하면 됩니다. Server Object

- production, development 환경의 분리를 위해 다수의 server 를 입력할 수 있습니다.

+) definition / host, basePath ?

- 간혹 다른 예제를 보면 host, basePath 등을 사용하는 예제가 있는데, 이는 swagger : '2.0' 을 사용할 때 사용했던 key 값이므로 일단 저는 해당부분을 무시하고 진행하겠습니다.

 

apis

- swagger jsdoc 이 주석을 확인할 수있도록 api 의 주소를 명시해줍니다. 

 

+) 필자는 swagger.js 의 경로가 src 내부에 있으므로 처음엔 ./app.js 로 시도했었는데, ./src/app.js 로 바꾸고 난 후 정상적으로 동작하는 걸 확인할 수 있었습니다. jsdoc 이 경로를 찾는 로직이 이래서 삽질을 좀 했네요 🥲

첫 swagger ui 등장!

Swagger 주석 작성

- 이제 본격적으로 API 들을 하나하나 추가해보겠습니다.

- swagger 에 정보를 전달하는 방법으로 주석을 작성합니다. @swagger 와 함께 작성을 하면 jsdoc 이 해당 주석을 읽어 api-docs 에 추가해줍니다. 기본적인 틀은 아래와 같습니다.

 

/**
 * @swagger
 *  /todo:
 *    get:
 *      summary: "전체 Todo 검색"
 *      tags: [Todo]
 *      responses:
 *        "200":
 *          description: 전체 Todo 검색
 *          content:
 *            application/json:
 *              schema:
 *                type: object
 *                properties:
 *                  data:
 *                    type: array
 *                    items:
 *                      type: object
 *                      properties:
 *                        id:
 *                          type: integer
 *                          description: todo id
 *                        subject:
 *                          type: string
 *                          description: todo subject
 */
app.get('/todo', (req, res) => {
  res.send({ data: fakeTodos });
});

@swagger 바로 아래에 path 를 지정하면 해당 부분이 보여지고, method 를 들여쓰기하여 작성하면 GET, PUT, DELETE 등 method 에 따라 색상이 결정되어 화면에 보여집니다.

tags 를 배열형태로 넘겨주면 위 타이틀에 tag 별로 API 가 정렬되는 것을 볼 수있고, 추가로 summary 를 작성하여 해당 API 가 어떤 것인지 명시할 수 있습니다.

 

responses 도 마찬가지로 아래에 status code 를 작성하고, 설명과 response 의 content 에 대한 정보를 작성할 수 있습니다.

components

- swagger.js 의 설정과 작성법을 알았으니 schemas 들을 만들 차례입니다.

components 는 OpenAPI 3.0 에서 부터 도입된 개념으로 

schema 는 꼭 만들 필요는 없습니다. 하지만 schema 를 만드는 것이 재사용에 유리합니다.

 

// components 사용 X

/**
 * @swagger
 *  /todo:
 *    get:
 *      summary: "전체 Todo 검색"
 *      tags: [Todo]
 *      responses:
 *        "200":
 *          description: 전체 Todo 검색
 *          content:
 *            application/json:
 *              schema:
 *                type: object
 *                properties:
 *                  data:
 *                    type: array
 *                    items:
 *                      type: object
 *                      properties:
 *                        id:
 *                          type: integer
 *                          description: todo id
 *                        subject:
 *                          type: string
 *                          description: todo subject
 */
app.get('/todo', (req, res) => {
  res.send({ data: fakeTodos });
});

// components 사용

/**
 * @swagger
 *  /todo:
 *    get:
 *      summary: "전체 Todo 검색"
 *      tags: [Todo]
 *      responses:
 *        "200":
 *          description: 전체 Todo 검색
 *          content:
 *            application/json:
 *              schema:
 *                type: object
 *                properties:
 *                  data:
 *                    type: array
 *                    items:
 *                      $ref: '#/components/schemas/Todo'
 */
app.get('/todo', (req, res) => {
  res.send({ data: fakeTodos });
});

- src/swagger/todo.yaml

components:
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: integer
          description: todo id
        subject:
          type: string
          description: todo subject

items 에 해당하는 Todo 를 ref 로 불러와서 사용할 수 있습니다.

만약 단 하나의 todo 를 불러오는 API 를 만든다면 해당 Todo 를 재사용할 수 있겠죠?

schema 가 필요한 곳이라면, yaml 파일을 만들어 사용할 수 있습니다.

 

// app.js

/**
 * @swagger
 *  /todo:
 *    post:
 *      summary: "Todo 생성"
 *      tags: [Todo]
 *      requestBody:
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/CreateTodo'
 *      responses:
 *        "201":
 *          description: 새로운 Todo 생성
 */
app.post('/todo', (req, res) => {
  const { subject } = req.body;
  const newTodoId = lastTodoId + 1;
  lastTodoId += 1;
  const newTodo = { id: newTodoId, subject };
  fakeTodos = [...fakeTodos, newTodo];
  res.sendStatus(201);
});


// createTodo.yaml

components:
  schemas:
    CreateTodo:
      type: object
      properties:
        subject:
          type: string
          description: Todo 내용

router 분리

지금까지 Swagger 기본 사용법에 대해선 다 익혔다고 생각합니다. 현재까지의 코드는 여기서 볼 수 있습니다.

아래부터는 Swagger 를 사용하면서 불편하다고 느꼈던 점을 개선해나가는 과정을 기술합니다.

 

주석의 양을 보시면 아시겠지만 여기서 Swagger 의 단점이 보입니다.

API 몇개만 작성하면 코드의 줄이 너무 길어져서 가독성이 안좋아진다는 거죠.

그래서 별도의 router 로 분리하는 작업을 가져보겠습니다.

 

일단 swagger.js 의 options apis 경로를 수정해줍니다.

const options = {
  swaggerDefinition: {
    openapi: '3.0.0',
    info: {
      title: 'Todo API',
      version: '1.0.0',
      description: 'Todo API with express',
    },
  },
  apis: ['./src/routers/*.js', './src/swagger/*'],
};

// './src/app.js' -> './src/routers/*.js'
├── package-lock.json
├── package.json
└── src
    ├── app.js
    ├── controllers
    │   └── todoController.js
    ├── routers
    │   └── todoRouter.js
    ├── swagger
    │   ├── createTodo.yaml
    │   └── todo.yaml
    └── swagger.js

- todo 관련 비즈니스 로직은 todoController 에 두고, todoRouter 는 router 로의 기능만 사용되어 가독성의 필요성을 줄였습니다.

// todoRouter.js

const express = require('express');
const {
  getTodos,
  createTodo,
  deleteTodo,
} = require('../controllers/todoController');

const todoRouter = express.Router();

/**
 * @swagger
 *  /todo:
 *    get:
 *      summary: "전체 Todo 검색"
 *      tags: [Todo]
 *      responses:
 *        "200":
 *          description: 전체 Todo 검색
 *          content:
 *            application/json:
 *              schema:
 *                type: object
 *                properties:
 *                  data:
 *                    type: array
 *                    items:
 *                      $ref: '#/components/schemas/Todo'
 */
todoRouter.get('/', getTodos);

/**
 * @swagger
 *  /todo:
 *    post:
 *      summary: "Todo 생성"
 *      tags: [Todo]
 *      requestBody:
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/CreateTodo'
 *      responses:
 *        "201":
 *          description: 새로운 Todo 생성
 */
todoRouter.post('/', createTodo);

/**
 * @swagger
 *  /todo/{id}:
 *    delete:
 *      summary: "특정 Todo 삭제"
 *      parameters:
 *        - in: path
 *          name: id
 *          required: true
 *      tags: [Todo]
 *      responses:
 *        "204":
 *          description: id 를 통해 특정 Todo 삭제
 */
todoRouter.delete('/:id', deleteTodo);

module.exports = todoRouter;
// app.js

const express = require('express');
const todoRouter = require('./routers/todoRouter');
const { swaggerUi, specs } = require('./swagger');

const PORT = 3000;

const app = express();
app.use(express.json()); // body-parser 내장된 것 사용
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

app.get('/', (req, res) => {
  res.send({ data: 'home' });
});

app.use('/todo', todoRouter);

app.listen(PORT, () => {
  console.log(`✅ Server listening on : ${PORT}`);
});

관련 코드는 여기서 볼 수 있습니다.

swagger 간단히 module 화

필자는 swagger 를 사용하면서 2개의 swagger 관련 라이브러리를 설치하고, 별도의 폴더를 만들어주고, 미들웨어 설정에 특정 함수를 사용해야 한다는 것이 복잡하게 느껴졌습니다. 이를 개선하기 위해서 지금까지 작성한 로직을 간단히 모듈화해보겠습니다.

 

코드는 여기서 볼 수 있습니다.

 

root 경로에 swagger.config.js 를 통해 options 를 관리하고, app.use('/api-docs', swagger) 로 사용 등 간편화를 목표로 하였습니다.

├── package-lock.json
├── package.json
├── src
│   ├── app.js
│   ├── controllers
│   │   └── todoController.js
│   └── routers
│       └── todoRouter.js
├── swagger
│   └── index.js
├── swagger-schemas
│   ├── createTodo.yaml
│   └── todo.yaml
└── swagger.config.js
// swagger.config.js

module.exports = {
  swaggerDefinition: {
    openapi: '3.0.0',
    info: {
      title: 'Todo API',
      version: '1.0.0',
      description: 'Todo API with express',
    },
  },
  apis: ['./src/routers/*.js', './swagger-schemas/*'],
};
// swagger/index.js

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

const options = require(path.resolve(process.env.PWD, 'swagger.config.js'));
const specs = swaggereJsdoc(options);

const swagger = [swaggerUi.serve, swaggerUi.setup(specs)];

module.exports = swagger;
// app.js

const express = require('express');
const swagger = require('../swagger');
const todoRouter = require('./routers/todoRouter');

const PORT = 3000;

const app = express();
app.use(express.json()); // body-parser 내장된 것 사용
app.use('/api-docs', swagger);

app.get('/', (req, res) => {
  res.send({ data: 'home' });
});

app.use('/todo', todoRouter);

app.listen(PORT, () => {
  console.log(`✅ Server listening on : ${PORT}`);
});

TMI) Swagger ? OpenAPI ?

Swagger는 처음 Wordnik 이라는 회사의 자체 API용 UI로 개발되었고 2015년초에 SmartBear 사에서 인수했습니다.

이후 SmartBear는 OpenAPI Initiative에 Swagger를 기부하면서 OpenAPI Specification으로 명칭을 변경하였습니다.

OpenAPI Revision History

그럼에도 Swagger 라는 명칭은 계속 쓰이는데, 아래와 같은 의미로 쓰이고 있습니다.

  • OpenAPI : 이전에 Swagger Specification으로 알려진 Specification 자체 RESTful API 디자인에 대한 정의(Specification)
  • Swagger : OpenAPI를 Implement하기 위한 도구 (SmartBear사의 tool)

 

참고

- https://joooohee.tistory.com/10

- https://gruuuuu.github.io/programming/openapi/

- https://spec.openapis.org/oas/v3.1.0

profile

파크로그

@파크park

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!