Swagger
오픈소스 기반 문서 자동화 도구로, 바로 테스팅해볼 수 있는 환경도 함께 지원한다.
졸업 프로젝트에서 프론트엔드 분과 협업할 필요가 있다. 이전에 진행했던 프로젝트에서는 API 요청 및 응답에 대한 문서화가 제대로 되지 않아 API 구조에 대한 의견을 주고 받다가 시간을 많이 낭비한 경험이 있다. 작성해 둔 API 관련 정보가 개발 과정에서 변화를 많이 겪었지만 이에 대한 문서는 최신화하지 않아 발생한 문제다.
따라서 이번 프로젝트에서는 swagger을 도입하여 이러한 시행착오를 줄이고자 한다.
@nestjs/swagger
https://docs.nestjs.com/openapi/introduction
nestjs는 추가 모듈 설치를 통해 swagger을 쉽게 사용할 수 있다.
npm install @nestjs/swagger
설정도 엄청 간편하다.
// swagger.ts 파일. 이걸 bootstrap 안에서 실행
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { INestApplication } from '@nestjs/common';
export function swaggerSetup(app: INestApplication) {
const config = new DocumentBuilder()
.setTitle('팀바나나 API 목록')
.setDescription('팀 바나나에서 사용하는 API입니다')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
}
// main.ts 파일
async function bootstrap() {
const app = await NestFactory.create(AppModule);
swaggerSetup(app);
// .. 이외 많은 use / 설정들
await app.listen(3000);
}
여기까지 진행하면 swagger 모듈이 우리 환경에 연결된 상태이다. swagger은 2가지 방법으로 접근 가능하다.
- http://내-주소/api: 웹 기반 사이트로 이동
- http://내-주소/api-json: 현재 작성된 swagger 문서에 대한 json 형식 제공
간단한 사용법
- 따로 설정하지 않으면 .dto.ts 또는 .entity.ts로 끝나는 파일을 스키마로 취급한다.
- 따로 설정하지 않으면 .controller.ts로 끝나는 파일에서만 API 정보를 가져온다.
- 주석을 문서로 변경하는 옵션은 nest-cli.json 옵션에서 따로 켜줘야 활성화된다.
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [{
"name":"@nestjs/swagger",
"options": {
"introspectComments": true,
"controllerKeyOfComment": "summary"
}
}]
}
}
- name: @nestjs/swagger
- options:
- introspectComments: true
- controllerKeyOfComment: "summary"
위 설정에서 introspectComments을 활성화하면 작성한 주석의 내용을 Swagger에 반영할 수 있게 된다. controllerKeyOfComment를 summary로 지정하면 주석에 작성한 내용이 API 제목 부분에 반영된다.
@ApiOperation({
summary: '이건 요약',
description: '이건 묘사',
})
@Post()
@ApiResponse({
description: '생성한 댓글',
status: 201,
type: () => OutCommentDto,
})
async createComment(@Body() dto: createCommentDto) {
const comment = await this.commentService.create(dto);
return comment;
}
요약 부분은 Swagger UI를 처음 봤을 때 각 API에 대해 최초로 이해하는데 큰 역할을 수행한다. 반면 description의 경우 각 API를 눌러야만 드러나므로, 각 API를 좀 더 자세하게 이해할 때 필요하다. 현재 프로젝트의 경우 API가 그렇게 복잡하지 않기 때문에 description까지 작성할 필요가 없다고 생각해서, 주석과 요약을 통일하기 위해 위처럼 설정했다.
주석 옵션을 반드시 켤 필요는 없다. 나는 나중에 각 프로퍼티나 메서드의 역할을 상기할 수 있도록 간단하게 주석을 추가하는 것을 선호한다. 그런데 swagger을 사용할 때는 @ApiProperty 또는 @ApiOperation에 주석에 들어갈 내용을 작성하기 때문에 정작 내가 개발할 때 참고하기는 어려워지는 불편함이 있어 주석 옵션을 켜는 것이 더 좋다.
주석 옵션을 사용하지 않더라도 플러그인 자체는 추가하는 것이 좋다. 플러그인이 없다면 @ApiProperty가 적용되지 않은 프로퍼티가 Swagger UI에 노출되지 않는다. dto를 만들 때마다 쓸모없는 프로퍼티를 붙일 생각이 아니라면 최소한 플러그인은 추가하자.
// 사용 된 Dto
class ArticleContentDto {
@IsString()
content: string;
@IsNumber()
score: number;
}
@nestjs/swagger 플러그인을 사용하는 경우
nest-cli가 다음 동작을 처리해준다고 한다.
- @ApiHideProperty 로 지정되지 않은 모든 DTO의 프로퍼티에 @ApiProperty 데코레이터를 추가한다.
- hello?: string 처럼 ?가 달린 경우 required: false로 지정한다.
- 타입에 기반하여 타입 / enum / array 등을 지정한다. (타입 알아서 지정해준다는 의미로 보임)
- 프로퍼티에 기본 값이 지정되어 있다면, 해당 값을 default로 설정한다.
- class-validator에 지정된 규칙을 추가한다.
- 모든 엔드포인트에 대해 적절한 status, type을 지정한다. (response model)
- 주석에 기반하여 description을 생성한다. (if introspectComments set to true)
- 주석에 기반하여 example 값을 추가한다. (if introspectComments set to true)
많은 것들을 처리해주기 때문에 어지간한 기능은 주석 + 타입스크립트 문법 수준에서 처리되서 편하다.
export class createCommentDto {
/**
* 댓글 생성일
* @example '2022-03-31'
*/
@IsDateString()
createdAt: Date;
/**
* 댓글 내용
* @example '오늘은 날씨가 좋아요'
*/
@IsString()
content: string;
/**
* 공감수
* @example 10597
*/
@IsNumber()
sympathy: number;
/**
* 비공감수
* @example 10597
*/
@IsNumber()
antipathy: number;
/**
* 뉴스 링크
* @example https://www.naver.com
*/
@IsString()
news_link: string;
/**
* 댓글의 대표 감정
* @example happiness
*/
@IsString()
emotion: string;
/**
* 연관성 있는 기사 내 문장들
*/
@ApiProperty({
isArray: true,
type: () => [ArticleContentDto],
})
@ValidateNested()
@Type(() => ArticleContentDto) // nested 처리하기 위한 용도
news_sentences: [ArticleContentDto];
}
class ArticleContentDto {
/**
* 기사 내 문장
* @example 한편 김창섭 기획실장은 차기 디렉터로...
*/
@IsString()
content: string;
/**
* 댓글과 기사의 연관도. -1 ~ 1 사이의 값.
* @example 0.7
*/
@IsNumber()
score: number;
}
위에서 유일하게 @ApiProperty를 지정해 둔 위치는 다른 객체 배열이 위치하는 부분이다. 저 부분은 데코레이터를 추가하지 않으면 그냥 문자열로 인식하기 때문에 isArray, type 옵션을 명시했다.
위 작성한 내용을 ApiProperty 기반으로 작성하면 다음과 같다.
export class createCommentDto {
@ApiProperty({
description: '댓글 생성일',
example: '2022-03-31',
})
@IsDateString()
createdAt: Date;
@ApiProperty({
description: '댓글 내용',
example: '오늘은 날씨가 좋아요',
})
@IsString()
content: string;
@ApiProperty({
description: '공감수',
example: 10597,
})
@IsNumber()
sympathy: number;
@ApiProperty({
description: '비공감수',
example: 10597,
})
@IsNumber()
antipathy: number;
@ApiProperty({
description: '뉴스 링크',
example: 'https://naver.com',
})
@IsString()
news_link: string;
@ApiProperty({
description: '댓글의 대표 감정',
example: 'happiness',
})
@IsString()
emotion: string;
@ApiProperty({
description: '연관성 있는 기사 내 문장들',
isArray: true,
type: () => [ArticleContentDto],
})
@ValidateNested()
@Type(() => ArticleContentDto) // nested 처리하기 위한 용도
news_sentences: [ArticleContentDto];
}
class ArticleContentDto {
@ApiProperty({
description: '기사 내 문장',
example: '한편 김창섭 기획실장은 차기 디렉터로...'
})
@IsString()
content: string;
@ApiProperty({
description: '댓글과 기사의 연관도. -1 ~ 1 사이의 값',
example: 0.7,
})
@IsNumber()
score: number;
}
데코레이터를 사용하는 것도 괜찮긴 한데, 이 경우 각 프로퍼티에 대한 설명이 주석이 없어서 개발하는 당사자 입장에서는 이게 뭔지 헷갈릴 수 있다.
import { Controller, Post, Get, Body, Query, Param } from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { createCommentDto } from './dtos/create-comment.dto';
import { GetCommentsQueriesDto } from './dtos/get-comments-query.dto';
import { AnalysisCommentService } from './analysis-comment.service';
import { OutCommentDto } from './dtos/out-comment.dto';
import { ObjectId } from 'mongodb';
@ApiTags('Comment')
@Controller('comment')
export class AnalysisCommentController {
constructor(private commentService: AnalysisCommentService) {}
/**
* 댓글 정보를 받아 DB에 저장, 생성된 댓글 반환
*/
@Post()
@ApiResponse({
description: '생성한 댓글',
status: 201,
type: () => OutCommentDto,
})
async createComment(@Body() dto: createCommentDto) {
const comment = await this.commentService.create(dto);
return comment;
}
/**
* 쿼리 정보 기반으로 댓글 정보 가져옴
*/
@ApiOperation({
description: '존재하는 쿼리 A, B, C를 지정하여 댓글을 가져올 수 있습니다',
})
@Get()
@ApiResponse({
description: '가져온 댓글 목록',
status: 200,
// type: () => [OutCommentDto],
})
async getComments(@Query() q: GetCommentsQueriesDto) {
console.log(q.search, typeof q.search);
console.log(q.pno, typeof q.pno);
console.log(q.psize, typeof q.psize);
return 'hello';
}
/**
* summary: 'param을 id로 하는 댓글 가져옴',
*/
@Get(':keyword')
@ApiParam({
name: 'keyword',
description: '키워드',
example: '윤석열',
})
async getCommentsById(@Param('keyword') id: ObjectId) {
return id + 'hello';
}
}
controller 부분도 크게 다르지 않다. @ApiOperation을 통해 각 API에 대한 설명을 추가할 수 있고, @ApiResponse을 통해 응답 값이나 형식에 대한 부분을 명시적으로 언급할 수 있다.
body, query 등 Dto에 의해 처리되는 경우 따로 명시하지 않아도 SwaggerUI 상의 적절한 위치에 삽입하고, 만약 개별적으로 명시하고 싶다면 @ApiBody, @ApiParam, @ApiQuery 데코레이터 등을 이용하여 설정할 수 있다.
생성된 문서를 보고 나니까 고된 시간이 보답받는 느낌이다. Swagger 도입을 통해 팀원들과 명확하게 소통할 수 있기를 기대한다...
'javascript' 카테고리의 다른 글
[javascript] 호이스팅, var, let, const (1) | 2023.09.20 |
---|---|
[자바스크립트] 시간 차이 측정하기 (0) | 2023.01.21 |