2차 프로젝트 진행 중에 굉장한 삽질을 했다.
정말 구글링과 ChatGPT만을 이용해 정보 수집만 8시간 정도 했던 것 같다.
1차 프로젝트에서는 API 명세서를 구글 스프레드 시트로 작성해서 관리했는데, 너무 불편했다.
수동으로 하나하나 추가/수정/삭제를 해줘야 했기 때문에 휴먼 에러가 발생할 가능성이 높았다.
이번 2차 프로젝트를 진행하면서는 Swagger라는 API Docs 라이브러리를 사용해보자는 의견이 나왔고, 실제로 적용하는 것 까지는 구글링으로 크게 어렵지는 않았다.
하지만 annotation 지옥이라고 스스로 명명한 현상을 겪고, 파일을 분리해서 적용해보고 싶었는데 여간 쉽지 않았다.
잘 되지 않아 시간을 잡아 먹을 거라고 판단해서, annotation 지옥에 빠질지언정 Swagger는 사용해야겠다 싶어서 포기하고 다른 것에 집중하려던 찰나에 갑자기 분리가 가능한 방법이 생각났고, 성공했다.
※ annotation 지옥 코드 예시 (실제 작성)
저 @swagger 라는 어노테이션 아래에 엄청난 주석이 달리게 된다.. 이것을 그냥 개인적으로 annotation 지옥이라고 명명했다.
/**
* @swagger
* paths:
* /api/group:
* post:
* summary: "모임 생성"
* description: "모임 생성"
* tags: ["Group"]
* requestBody:
* required: true
* description: "모임 생성시 필요한 데이터"
* content:
* application/json:
* schema:
* type: object
* required:
* - gName
* properties:
* gName:
* type: string
* description: "모임명 (Unique)"
* example: "스터디"
* gDesc:
* type: string
* description: "모임 설명 (TEXT 타입으로 지정되어 있으므로, 최대 66,535 저장 가능)"
* example: "Node.js 스터디 모임입니다!"
* gDday:
* type: string
* description: "모임 디데이 (DATE 타입)"
* example: "2023-10-27"
* gMaxMem:
* type: integer
* description: "모임 최대 인원"
* example: "10"
* gCategory:
* type: string
* description: "<p>모임 카테고리(분야) <br/> ex: 운동 / re: 독서 / st: 스터디 / eco: 경제 / lan: 언어 / cert: 자격증 / it: IT / etc: 기타</p>"
* example: "st"
* gCoverImg:
* type: string
* description: "모임 커버 이미지(서버에 업로드된 커버 이미지 경로)"
* example: "https://media.istockphoto.com/id/1455038582/photo/rain-drop.webp?b=1&s=170667a&w=0&k=20&c=0czyea70W_dp0R70hJE4m1ljfmb4qgXhj_j6Rj9NsuY="
*
* responses:
* 200:
* description: "모임 생성 완료"
* content:
* application/json:
* schema:
* type: object
* properties:
* isSuccess:
* type: boolean
*/
router.post('/', controller.postGroup); // 모임 생성
이 포스트는 처음부터 swagger를 node.js의 express 라이브러리를 사용해서 적용하는, 다만 annotation의 지옥이 아닌 파일을 분리해서 깔끔하게 적용하는 방법에 대해서 설명하려 한다.
1. 라이브러리 설치
express, swagger-ui-express, swagger-jsdoc을 설치
# express, swagger-ui-express, swagger-jsdoc
$ npm i express swagger-ui-express swagger-jsdoc
라이브러리에 대한 설명은 다음과 같다.
- express : express 라이브러리
- swagger-ui-express : express에서 swagger를 ui로 그나마 이쁘게 보기 위한 라이브러리
- swagger-jsdoc : 위에서 본 것 처럼 /** @swagger */ 주석을 이용해서 swagger에 문서화하기 위한 라이브러리
2. Swagger 정의
여기서는 구글링과 GPT를 사용해서 검색해보면 알겠지만, 방법이 갈리는 부분이다.
검색해본 결과로는 크게 3가지인 yaml, json, js 파일로 관리하는 방법이 많았다. (특히 yaml, json)
이번 프로젝트에서는 dotenv 모듈을 통해서 process.env.###을 통해서 관리하고 싶어서 js 파일로 작성하는 방법을 선택했다.
코드를 보면 바로 이해가 될 것이다.
const swaggerUi = require('swagger-ui-express');
const swaggerJSDoc = require('swagger-jsdoc');
// config.js 파일에 필요한 정보를 작성했다.
const config = require(__dirname + '/../../config/config.js')[
process.env.NODE_ENV // package.json에서 "scripts" > "start"에 NODE_ENV를 지정함
];
// swagger-config.yaml파일로 설정하지 않은 이유는
// 보안을 위해 서버 URL과 PORT를 숨기기 위함
const { serverUrl, serverPort } = config;
const options = {
swaggerDefinition: {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Motimates OpenAPI', // Your Project Title
description: 'Motimates RestFul API 클라이언트 UI', // Your Project Description
contact: {
name: 'Motimates',
email: 'eoeung113@gmail.com',
},
},
servers: [
{
url: `${serverUrl}:${serverPort}`, // 요청 URL
},
],
},
apis: ['./routes/*.js', './modules/swagger/**/*.yaml'], // swagger와 연동할 파일 작성
};
const specs = swaggerJSDoc(options);
module.exports = { swaggerUi, specs };
코드를 해석해보자면,
(1) swagger-ui-express와 swagger-jsdoc을 가져온다.
(2) config로 우리 프로젝트와 관련된 설정 정보를 호출한다.
(3) swaggerJSDoc 변수에 option을 넣어줘야 하는데,
그 옵션은 swaggerDefinition이라고 명명한 Object 타입과 apis라는 변수 값을 Object로 만들어준다.
(4) openapi에 swagger 버전을 명명해야한다.
현재 알기로는 2.0과 3.0 버전이 있는 것으로 알고 있음
(5) servers에는 배열로 만들고 여러 url을 객체로 넣어줄 수 있다.
(예를 들어 로컬, 개발, 배포 이렇게 3개도 넣어줄 수 있다.)
swaggerDefinition 객체는 크게 어렵지 않은데, 내가 삽질한 가장 큰 이유인 apis를 알아보려한다.
(6) apis는 swagger에 설정할 값이 있는 파일을 읽어오는 기능을 한다.
아까 위에서 봤던 /** @swagger */ 요 부분 안에 있는 내용들이 방금 말했던 swagger에 설정할 값 이라고 생각하면 된다.
실제로 swagger editor라는 사이트에 들어가면 어떻게 작성해야하는지 볼 수 있다.
https://editor.swagger.io/
Swagger Editor
editor.swagger.io
3. Swagger 설정값 정의
위에서 봤던 apis와 더불어서 Swagger 설정값을 정의하는 방법에 대해서 설명하려 한다.
크게 흐름부터 따라가자면 GPT의 도움 받아 임시로 짜놓은 코드를 보면 이해가 될 것이다.
[app.js]
// app.js
const express = require('express');
const app = express();
const port = 3000;
// Swagger 설정
const swaggerJSDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const swaggerOptions = {
swaggerDefinition: {
info: {
title: 'API Documentation',
version: '1.0.0',
description: 'API Documentation for your Express application',
},
},
apis: ['**/*.js'], // 여기에 라우터 파일의 경로를 설정
};
const swaggerSpec = swaggerJSDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// 루트 라우트
app.get('/', (req, res) => {
res.send('Hello, Swagger!');
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
위 코드는 GPT가 짜준 코드인데, 아까 우리 프로젝트에서 swagger 정의하는 값을 따로 js로 빼서 적용했다면 GPT는 예시에 매우 좋은, 흐름을 볼 수 있는 코드를 임시로 짜주었다.
swaggerOption까지는 똑같고 apis에 써있는 주석을 보면, 라우터 파일의 경로를 설정이라고 되어있는데 이게 반은 맞는 이야기이다.
내가 공유한 위의 코드에는 router 파일도 존재하지만, yaml파일도 존재한다. 즉, apis는 결국 위에서 설명한대로 swagger를 설정할 값을 찾아가는 역할을 한다고 생각하면 된다.
∴ swagger에 보여줄 내용의 값
그리고 app.use를 통해 미들웨어를 등록한다.
이 때 인자를 3개 받는다.
app.use('swagger로 접속할 UI', 'swagger ui를 서비스한다는 의미 같음', 'swagger 정의 값');
이렇게 미들웨어 등록까지 끝났는데, 마지막 제일 중요한 라우터에 설정하는 부분이 남아있다.
[users.js] : 라우터
// routes/users.js
/**
* @swagger
* /users:
* get:
* summary: Get a list of users
* description: Returns a list of users.
* responses:
* 200:
* description: A list of users.
*/
const express = require('express');
const router = express.Router();
// 실제 라우팅 로직은 여기에 추가
module.exports = router;
이 코드도 gpt가 짜준 임시 코드이다.
마찬가지로 /** @swagger */ 로 swagger에서 사용할 값임을 설정한다.
그 아래 내용을 설명하자면
/users: api 주소
get: method
summary: 개발자를 위한 요약 내용
description: swagger UI에서 보여지는 설명 내용
responses: 리턴값 (프론트에서 받을 내용)
이다.
여기에 빠진 내용이 많은데, 프론트에서 보내줄 때 queryString, requestBody 등 여러 방법이 존재해서 그 부분에 관한 내용은 따로 찾아보면 된다.
그리고 제일 중요한 점이 무조건 하위로 내용을 작성할 경우에는 공백(space)을 2번 주어야한다.
tab말고 space를 눌러야한다.
이게 하나라도 꼬이면 파일을 읽어오지 못해서 에러를 내뱉는다.
4. 파일 분리해서 yaml파일로 관리하기
제일 중요한 파일 분리이다.
이 부분은 yaml파일을 여러 개 만들어서 관리하기로 했다.
파일 구조는
modules
ㄴㄴswagger
ㄴㄴㄴㄴ requestBody
ㄴㄴㄴㄴㄴㄴGroupRequestBody.yaml
ㄴㄴㄴㄴ response
ㄴㄴㄴㄴㄴㄴGroupResponse.yaml
이렇게 지정하면, 위에서 봤던 코드에서 apis에
apis: ['./routes/*.js', './modules/swagger/**/*.yaml'], // swagger와 연동할 파일 작성
이런식으로 작성하면 된다.
(이 apis 파일 경로는 app.js를 기준으로 설정해줘야하는 것 같다.
이 부분에서 어떤 기준으로 파일 경로를 지정해야 하는지 몰라서 자꾸 에러가 났다.)
requestBody와 response 예시 코드를 보여주려고 한다.
[requestBody 예시 코드]
###################################################
################## [RequestBody] ##################
###################################################
components: # 컴포넌트 선언
schemas: # 스키마 정의
# 모임 생성시 필요한 req.body
postGroup: # POST '/api/group'
required:
- gName
- gDesc
- gDday
- gMaxMem
- gCategory
- mTitle
- mContent
- mLevel
type: object
description: 모임 생성 시 필요한 정보
properties: # req.body로 넘어온 값 정의
gName:
type: string
description: 모임명 (Unique)
example: Node 스터디 (중복 안됩니다!)
gDesc:
type: string
description: 모임에 대한 설명
example: Node.js 스터디 모임입니다!
gDday:
type: string
format: date-time
description: 모임 자체의 디데이
example: 2023-10-28
gMaxMem:
type: integer
format: int64
description: 모임 최대 인원
example: 10
gCategory:
type: string
description: ex = 운동 / re = 독서 / st = 스터디 / eco = 경제 / lan = 언어 / cert = 자격증 / it = IT / etc = 기타
example: st
gCoverImg:
type: string
description: 모임 커버 이미지
example: https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSr1_J07ruu0QuBhaD6HSDkvbQdW_OOENXmiA&usqp=CAU
mTitle:
type: string
description: 미션 제목
example: Node.js 강의 듣기
mContent:
type: string
description: 미션 내용
example: Node.js 강의 쳅터 1 듣고 오기
mLevel:
type: integer
description: 난이도에 따른 점수 부여 (상 = 5점, 중 = 3점, 하 = 1점)
example: 5
# 모임 수정시 필요한 req.body
patchGroup: # PATCH '/api/group'
required:
- gName
- gDesc
- gDday
- gMaxMem
- gCategory
type: object
description: 모임 수정 시 필요한 정보
properties: # req.body로 넘어온 값 정의
gSeq:
type: integer
format: int64
description: 모임 시퀀스
example: 1
gName:
type: string
description: 모임명 (Unique)
example: 정보처리기사 실기 대비반 (중복 안됩니다!)
gDesc:
type: string
description: 모임에 대한 설명
example: 정보처리기사 실기 대비 오프라인 모임입니다!
gDday:
type: string
format: date-time
description: 모임 자체의 디데이
example: 2023-10-31
gMaxMem:
type: integer
format: int64
description: 모임 최대 인원
example: 20
gCategory:
type: string
description: ex = 운동 / re = 독서 / st = 스터디 / eco = 경제 / lan = 언어 / cert = 자격증 / it = IT / etc = 기타
example: cert
gCoverImg:
type: string
description: 모임 커버 이미지
example: https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQVnwfCZtvVrf0NdXWT4YQp_aVEFlZ5-kuUfw&usqp=CAU
# 모임 삭제시 필요한 req.body
deleteGroup: # DELETE '/api/group'
required:
- gSeq
type: object
description: 모임 삭제 시 필요한 정보
properties: # req.body로 넘어온 값 정의
gSeq:
type: integer
format: int64
description: 모임 시퀀스
example: 1
[reponse 예시 코드]
###################################################
################## [Responses] ####################
###################################################
components:
schemas:
# 기본적인 응답 object (성공 여부, 응답 메시지)
groupApiResult:
required:
- isSuccess
- msg
type: object
description: 기본적인 응답 object (성공 여부, 응답 메시지)
properties: # res.json으로 정의한 값 정의
isSuccess:
type: boolean
description: 성공 여부
example: true
msg:
type: string
description: 응답 메시지
example: 성공
# 생성된 모임 정보
postGroupResult: # POST '/api/group'
type: object
description: 생성된 모임 정보
properties: # res.json으로 정의한 값 정의
gSeq:
type: integer
format: int64
description: 모임 시퀀스
example: 1
gName:
type: string
description: 모임명 (Unique)
example: Node 스터디
gDesc:
type: string
description: 모임에 대한 설명
example: Node.js 스터디 모임입니다!
gDday:
type: string
format: date-time
description: 모임 자체의 디데이
example: 2023-10-28
gMaxMem:
type: integer
format: int64
description: 모임 최대 인원
example: 10
gCategory:
type: string
description: ex = 운동 / re = 독서 / st = 스터디 / eco = 경제 / lan = 언어 / cert = 자격증 / it = IT / etc = 기타
example: st
gCoverImg:
type: string
description: 모임 커버 이미지
example: https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSr1_J07ruu0QuBhaD6HSDkvbQdW_OOENXmiA&usqp=CAU
gTotalScore:
type: integer
format: int64
description: 현재 미션 총 점수(현재 진행되는 미션에 대한 총 점수)
example: 15
createdAt:
type: string
format: date-time
description: 모임 생성 시간
example: 2023-10-28
updatedAt:
type: string
format: date-time
description: 모임 수정 시간
example: 2023-10-28
# 수정된 모임 정보
patchGroupResult: # PATCH '/api/group'
type: object
description: 수정된 모임 정보
properties: # res.json으로 정의한 값 정의
gSeq:
type: integer
format: int64
description: 모임 시퀀스
example: 1
gName:
type: string
description: 모임명 (Unique)
example: 정보처리기사 실기 대비반 (중복 안됩니다!)
gDesc:
type: string
description: 모임에 대한 설명
example: 정보처리기사 실기 대비 오프라인 모임입니다!
gDday:
type: string
format: date-time
description: 모임 자체의 디데이
example: 2023-10-31
gMaxMem:
type: integer
format: int64
description: 모임 최대 인원
example: 20
gCategory:
type: string
description: ex = 운동 / re = 독서 / st = 스터디 / eco = 경제 / lan = 언어 / cert = 자격증 / it = IT / etc = 기타
example: cert
gCoverImg:
type: string
description: 모임 커버 이미지
example: https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQVnwfCZtvVrf0NdXWT4YQp_aVEFlZ5-kuUfw&usqp=CAU
gTotalScore:
type: integer
format: int64
description: 현재 미션 총 점수(현재 진행되는 미션에 대한 총 점수)
example: 15
createdAt:
type: string
format: date-time
description: 모임 생성 시간
example: 2023-10-28
updatedAt:
type: string
format: date-time
description: 모임 수정 시간
example: 2023-10-31
※ **은 하위의 모든 디렉토리, 파일을 의미한다.
###/*/*.yaml 과 같은 형식으로 적어줬더니, modules/swagger 하위에 있는, 맨 위에 디렉토리의 yaml 파일만 적용되어서 수정했다.
이렇게 설정하면 아래와 같은 화면이 출력된다.
아래 그림은 위의 yaml파일에 있는
component:
schemas:
하위 내용이 swagger ui에 나오는 그림이다.
5. swagger 로그인 적용
위 처럼 swagger에 접속하려 할 때, 로그인을 설정하는 부분이 있다.
이 부분은 app.js에서 app.use()로 미들웨어 등록하는 부분의 코드만 조금 수정해주면 된다.
const eba = require('express-basic-auth');
// ... 코드 생략 ...
// 4) swagger
// 첫 인자로 받은 경로로 접속하면 swagger UI가 보임
app.use(
'/api-docs',
eba({
// swagger 로그인 설정
challenge: true,
users: { admin: process.env.SWAGGER_PW },
}),
swaggerUi.serve,
swaggerUi.setup(specs, { explorer: true }) // swagger 검색 기능
);
express-basic-auth 라는 라이브러리를 설치하고, app.use()에서 경로 다음으로 인자를 하나 받아주면 된다.
'IT > SeSAC' 카테고리의 다른 글
[새싹x코딩온] 2차 프로젝트 회고록 (231023 ~ 1110) (0) | 2023.11.16 |
---|---|
[새싹x코딩온] Express x socket.io로 간단한 단체 채팅 만들기 (라우터 통해서 값을 전달하기) (4) | 2023.11.09 |
[새싹x코딩온] 1차 프로젝트 회고록 (230904 ~ 0922) (1) | 2023.10.19 |
[새싹x코딩온] 웹 개발자 부트캠프 과정 11일차 정리 | Process, Thread, Node.js의 특징, Module다루기, 구조 분해 할당 (0) | 2023.08.22 |
[새싹x코딩온] 웹 개발자 부트캠프 과정 10일차 정리 | Protocol, Web Server & WAS, URI & URL, Domain, DNS (0) | 2023.08.07 |