API 명세를 보통 어디에, 어떻게 작성하는가? 나는 다음의 세 가지의 조건을 모두 충족해야 한다고 생각한다.
1. 프론트엔드와 백엔드가 모두 접근 가능해야 한다.
2. 수정이 용이해야 한다.
3. 문서와 코드는 항상 동일해야 한다.
이 조건들에 따라 나는 팀원과 같이 사용하는 Notion과 같은 공용 문서 도구, 또는 모든 사람들이 접근가능하며 개발 시에 자주 들어가는 Github의 Wiki에 API 명세를 작성하곤 했다. 하지만 위 두 가지 방식은 3번째 조건을 지키기 어려웠다.
여기서 Swagger가 등장한다. 개발자가 설정한 URL로 (자동 배포를 한 경우) 언제나 접근이 가능하며 API 명세를 코드로 나타내어 수정이 용이하면서도 명세와 코드를 IDE 상에서 쉽게 비교하여 통일시킬 수 있다.
프로젝트에 swagger를 적용시키는 과정만 보고 싶다면 적용 과정로 바로 이동하면 된다. 나머지는 뭐 그냥 주저리주저리..
Swagger는 OpenAPI 정의를 사용하여 API를 코드로 작성하고, Swagger UI를 사용하여 프로젝트에서 지정한 URL에 문서를 보여줄 수 있게 해주는 도구이다. Swagger UI가 제공해준 페이지를 통해 서버로 요청을 보낼 수 도 있고 응답을 확인할 수 도 있다.
Swagger에 대한 정확한 정보는 공식 문서를 읽어보길 바란다. 그 중에서도 Basic Structure와 Components Section 문서를 읽으면 어떻게 작성해야 하는지 금방 파악할 수 있을 것이다.
해당 프로젝트는 TypeScript, Node.js, express를 사용한다.
필요한 패키지들을 먼저 설치해준다.
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
타입을 반환하여 문제가 없어 선택하게 됐다.
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"
},
start
와 dev
는 원래 있는 스크립트이다. api-docs
가 바로 ./src/swagger/openapi.yaml
부터 읽어서 관련된 모든 파일을 통합해 build/swagger.yaml
로 만들라는 스크립트이다.
또한 npm run dev
명령어를 실행할 때 항상 swagger가 최신으로 유지되도록 predev
스크립트를 추가했다.
이제 npm run dev
을 실행시켜보면 다음과 같은 화면을 볼 수 있다.
위에서 만들어준 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
를 실행한 다음 새로고침을 눌러보면 다음과 같은 화면을 볼 수 있다.
간단한 예시로 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 명세를 때려박을 수 도 있다. 문제는 두 가지 방법 모두 단점이 보였다는 것이다.
swagger-jsdoc
사용해서 API 코드 상단에 명세 작성위 코드는 단 하나의 API 에 대한 명세이다. 심지어 전부가 아닌 1/2 정도이다. Swagger는 재사용가능한 파라미터, 응답, 스키마를 분리하여 참조하는 형식으로 코드의 길이를 줄일 수 있는데 (내가 못찾은 것 같긴 하지만) swagger-jsdoc
는 다른 파일을 참조할 수 없었다. 따라서 보통 하나의 컨트롤러 파일에 CRUD 네 가지의 미들웨어가 존재한다고 치면, 위 사진에서 보이는 명세의 최소 5배는 추가될 것이다. 당연히 파일이 더럽겠죠?
yaml
또는 json
파일 사용하나의 파일에 모든 API 명세, 파라미터, 스키마, 응답을 모두 적는 방법이다. yaml
이나 json
파일 모두 들여쓰기가 중요한데, 하나의 파일에 수많은 명세를 작성하다보면 들여쓰기로 인한 오류는 물론, 내가 보고자 하는 API 명세를 찾기는 굉장히 어렵고 눈 아플 것이다.
그래서 우리는
yaml을 여러 개로 분리하여 명세와 개발을 진행하고, API 명세를 UI로 확인하고자 할 때는 이를 하나의 yaml 파일로 합치는 방식을 채택했다. 이 방법도 물론 단점이 있다. 개발 시에는 nodemon
을 사용하는데, yaml
파일이 변경되면 새로운 하나의 yaml을 생성해야 하는 것을 nodemon
이 알 수 없다. 따라서 API 명세에 대한 코드가 변경되면 직접 파일을 생성해줘야 한다.
잘 읽었습니다ㅎㅎ