NestJS 帶你飛! 시리즈 번역 26# Swagger(상)

2023. 7. 1. 19:47개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.

 

현재 프론트엔드 개발자로 일하고 있다면 백엔드 개발자에게 API 문서를 요구해본적이 있을겁니다.

반대로 백엔드 개발자로 일하고 있다면 팀이 작성한 API에 대해 API 문서를 작성해야 합니다. 

대부분의 사람들은 시간을 들여 정리해 문서화 하는 작업을 싫어할겁니다.

매 버전에 대한 유지보수 또한 시간과 힘이 드는 작업임은 분명합니다.
개발자들이 직접 문서화 하는 방법밖엔 없는걸까요? 다른 획기적인 방법은 없는걸까요?

이에 대한 명쾌한 해결 방법은 개발자라면 한번쯤 들어본 적이 있거나 사용해본 적이 있는 Swagger입니다.

 

Swagger란 무엇인가요?

사진 출처 : https://swagger.io/

 Swagger는 API를 시각화하여 보여주는 도구중 하나입니다. 간단하게 말해 각각의 API 전부를 나열해줍니다.
(각각 API에 필요한 파라미터나 파라미터의 양식도 전부)
심지어 해당 페이지를 통해 API 서버에게 직접 요청을 보낼 수도 있습니다.
이는 Postman과 유사한 기능을 하며, API 문서 유지보수 비용을 획기적으로 줄여줄 뿐만 아니라 프론트/백엔드단의 개발 효율 증진을 기대할 수 있습니다.

 

Swagger 입문

Nest는 Swagger를 패킹하여 모듈로써 제공합니다. npm을 통해 설치할 수 있습니다.

그러나 유의해야할 점 하나는 Nest에서 제작한 모듈 외에 Swagger의 패키지를 하나 더 설치해야 합니다.

 

$ npm install @nestjs/swagger swagger-ui-express
주의: Fastify를 기반으로 사용할 경우 swagger-ui-express가 아닌 fastify-swagger를 설치해야 합니다!

 

이어서 main.ts에서 초기화를 진행해야 합니다. DocumentBuilder를 통해 기본 문서 양식을 생성해주고,
설정해주어야 할 내용으로는 제목, 설명, 버전 등이 있습니다.
SwaggerModulecreateDocument 메서드로 문서를 생성해주고 SwaggerModulesetup 메서드를 사용하여 실행 시켜주면 됩니다. setup에는 4개의 파라미터를 받아야 하는데 파라미터들은 다음과 같습니다.

 

  • path: Swagger UI의 라우팅 주소입니다.
  • app: 바인딩 할 Nest App의 인스턴스를 입력합니다.
  • document: 초기화 된 문서를 입력합니다. 즉 createDocument가 생성한 문서입니다.
  • options: UI를 구성할 경우 선택적으로 입력이 가능합니다.
    받을 수 있는 파라미터의 양식은 SwaggerCustomOptions입니다.
주의: UI 구성은 잠시 후에 다시 설명하겠습니다.

 

아래는 간단한 예제입니다.

import { NestFactory } from '@nestjs/core';
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  setupSwagger(app);
  await app.listen(3000);
}

function setupSwagger(app: INestApplication) {
  const builder = new DocumentBuilder();
  const config = builder
    .setTitle('TodoList')
    .setDescription('This is a basic Swagger document.')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);
}

bootstrap();

 

서버를 키고 http://127.0.0.1:3000/api 주소대로 접속하게되면 아래와 같은 화면이 보입니다.

해당 Swagger의 문서를 JSON 파일로 받아오고 싶다면 http://127.0.0.1:3000/<PATH>-json 주소를 통해 받아올 수 있습니다. 아래는 해당 예제입니다.  

 

UI 설정 항목

아래의 UI 설정 항목들을 적절히 사용해 Swagger UI를 입맛에 맞게 바꿀 수 있습니다.

중요한 사항과 많이 쓰이는 옵션들은 다음과 같습니다.

  • explorer: 검색창을 열지 말지 여부를 나타내며 기본값은 false입니다.
  • swaggerOptions: Swagger 기타 설정 항목들입니다. 자세한 내용은 공식 문서를 참고해주세요.
  • customCss: Swagger UI의 CSS를 커스터마이징 할 수 있습니다.
  • customCssUrl: 커스텀한 Swagger UI의 CSS URL를 적을 수 있습니다.
  • customJs: Swagger UI를 조작하기 위해 사용자 정의 JavaScript를 사용합니다.
  • customfavIcon: Swagger UI icon을 설정할 수 있습니다.
  • swaggerUrl: Swagger JSON 리소스의 URL을 제공합니다.
  • customSiteTitle: Swagger UI의 사용자 정의 제목을 설정합니다.
  • validatorUrl: Swagger의 Validator 리소스 URL을 제공합니다.
  •  

아래의 예제는 검색창을 true로 한 예제입니다.

import { NestFactory } from '@nestjs/core';
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerCustomOptions, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  setupSwagger(app);
  await app.listen(3000);
}

function setupSwagger(app: INestApplication) {
  const builder = new DocumentBuilder();
  const config = builder
    .setTitle('TodoList')
    .setDescription('This is a basic Swagger document.')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  const options: SwaggerCustomOptions = {
    explorer: true, // 開啟搜尋列
  };
  SwaggerModule.setup('api', app, document, options);
}

bootstrap();

 

다시 http://127.0.0.1:3000/api에 접속하보면 검색창이 나타난걸 확인할 수 있습니다.

API 파라미터 설계

SwaggerModule은 문서를 작성하는 과정에서 모든 Controller의 경로들을 검색하며 @Query, @Param, @Body와 같은 파라미터들을 해석하여 Swagger UI에 표시해줍니다. 이를 통해 해당 API에 필요한 모든 파라미터들을 나열할 수 있을뿐만 아니라 파라미터의 유형 또한 표시할 수 있습니다.


CLI를 통해 TodoModule, TodoControllerTodoService를 생성하겠습니다.

$ nest generate module features/todo
$ nest generate controller features/todo
$ nest generate service features/todo

 

이어서 TodoService에 배열을 하나 선언해 자료를 저장하고 getTodo 메서드를 통해 해당 내용들을 받아오겠습니다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {
  todos = [
    {
      id: 1,
      title: 'Ironman 13th',
      description: 'NestJS tutorial.',
      completed: false,
    },
  ];

  getTodo(id: string) {
    return this.todos.find((todo) => todo.id.toString() === id);
  }
}

 

이어서 TodoController의 내용을 수정하여 id를 받아 자료를 얻어오는 API를 설계하겠습니다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {
  todos = [
    {
      id: 1,
      title: '제목',
      description: 'NestJS tutorial.',
      completed: false,
    },
  ];

  getTodo(id: string) {
    return this.todos.find((todo) => todo.id.toString() === id);
  }
}

http://127.0.0.1:3000/api에 접속하면 방금 만든 API가 등록이 된걸 확인할 수 있습니다.

 

복잡한 파라미터 해석

SwaggerModule은 자동으로 파라미터의 유형을 해석할 수 있지만 비교적 복잡한 파라미터 유형에 대해서는 특별한 처리가 필요할 수 있습니다. 처리가 필요한 유형은 바로 아래에서 소개하겠습니다.

 

DTO

DTO는 객체 형식의 데이터 유형입니다.
SwaggerModule이 해당 객체의 파라미터들을 정상적으로 해석할 수 있도록 하려면 @ApiProperty 데코레이터를 사용해야 합니다.

 

먼저 src/features/todo 폴더 아래 dto 폴더를 생성해 create-todo.dto.ts를 만들어준 후 @ApiProperty 데코레이터들을 추가시켜줍니다.

import { ApiProperty } from '@nestjs/swagger';

export class CreateTodoDto {
  @ApiProperty()
  title: string;

  @ApiProperty()
  description: string;

  @ApiProperty()
  completed: boolean;
}

 

이어서 TodoService를 수정하여 createTodo 메서드를 통해 데이터를 추가하겠습니다.

import { Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';

@Injectable()
export class TodoService {
  todos = [
    {
      id: 1,
      title: '제목',
      description: 'NestJS tutorial.',
      completed: false,
    },
  ];

  createTodo(data: CreateTodoDto) {
    const todo = { id: this.todos.length + 1, ...data };
    this.todos.push(todo);
    return todo;
  }

  getTodo(id: string) {
    return this.todos.find((todo) => todo.id.toString() === id);
  }
}

 

TodoController에 API를 하나 더 추가해 데이터를 추가할 수 있게 하겠습니다.

 

import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { TodoService } from './todo.service';

import { CreateTodoDto } from './dto/create-todo.dto';

@Controller('todos')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Post()
  createTodo(@Body() data: CreateTodoDto) {
    return this.todoService.createTodo(data);
  }

  @Get(':id')
  getTodo(@Param('id') id: string) {
    return this.todoService.getTodo(id);
  }
}

 

브라우저를 열어 http://127.0.0.1:3000/api [POST] /todos를 누르면 파라미터 의 유형을 확인할 수 있습니다.

 

해당 속성에 대해 설명, 최대 길이와 같은 옵션들을 추가하고 싶다면 @ApiProperty 데코레이터에 매개변수를 지정할 수 있습니다. 여기서는

title의 최대 길이를 20으로 지정한 후 description의 최대 길이를 200으로 지정하고 각 속성의 역할을 설명 하겠습니다.

import { ApiProperty } from '@nestjs/swagger';

export class CreateTodoDto {
  @ApiProperty({
    maxLength: 20,
    description: 'Todo 的標題',
  })
  title: string;

  @ApiProperty({
    maxLength: 200,
    description: 'Todo의 설명',
  })
  description: string;

  @ApiProperty({
    description: 'Todo를 완성 했는지 여부',
  })
  completed: boolean;
}
주의: 더 자세한 사항은 공식 사이트를 참고해주세요.

 

http://127.0.0.1:3000/api에 접속하면 Schemas 안의 CreateTodoDto에선 아래와 같은 내용을 확인할 수 있습니다.

 

배열

배열도 역시 파싱이 불가능한 자료 유형입니다. DTO안에 배열 유형을 포함한 데이터가 있다면 역시 @ApiProperty 데코레이터를 통해 해결이 가능합니다. type을 주어 SwaggerModule이 해당 속성이 배열 속성임을 알려주면 됩니다.

 

CreateTodoDto를 수정하여 tags 속성을 추가하겠습니다.

import { ApiProperty } from '@nestjs/swagger';

export class CreateTodoDto {
  @ApiProperty({
    maxLength: 20,
    description: 'Todo의 제목',
  })
  title: string;

  @ApiProperty({
    maxLength: 200,
    description: 'Todo의 설명',
  })
  description: string;

  @ApiProperty({
    description: 'Todo를 완성 했는지 여부',
  })
  completed: boolean;

  @ApiProperty({
    type: [String],
    description: 'Todo의 태그 목록',
  })
  tags: string[];
}

 

http://127.0.0.1:3000/api에 접속해 Schema 안의 CreateTodoDto를 클릭하면 아래와 같은 결과를 확인할 수 있습니다.

 

아래 예시처럼 본문 데이터(Body)가 배열 형식인 경우엔

@ApiProperty를 사용해 파싱하는것은 적절하지 않습니다. 대신 해당 메서드에 @ApiBody 데코레이터를 추가하고 type[CreateTodoDto]와 같은 형식으로 지정해주어야 합니다.

 

TodoController를 수정하여 일괄로 데이터를 처리하는 API를 추가하겠습니다.

import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';

import { TodoService } from './todo.service';

import { CreateTodoDto } from './dto/create-todo.dto';

@Controller('todos')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Post()
  createTodo(@Body() data: CreateTodoDto) {
    return this.todoService.createTodo(data);
  }

  // 다중 업로드
  @ApiBody({ type: [CreateTodoDto] })
  @Post('bulk')
  createTodos(@Body() todos: CreateTodoDto[]) {
    return todos.map((todo) => this.todoService.createTodo(todo));
  }

  @Get(':id')
  getTodo(@Param('id') id: string) {
    return this.todoService.getTodo(id);
  }
}

 

http://127.0.0.1:3000/api에 접속하여 [POST] /todos/bulk를 검색하면 아래와 같이 파라미터의 유형을 확인할 수 있습니다. 

Enum

Enum 또한 특정한 형식으로 지정해주어야 합니다.
DTO를 예로 들어보면 @ApiProperty 데코레이터에서 enum을 특정 Enum으로 지정해주어야 합니다.

 

src/features/todo 폴더에 types 폴더를 생성한 후 priority.type.ts를 생성해주겠습니다.
*priority: 우선순위

export enum TodoPriority {
  HIGH = 'high',
  MEDIUM = 'medium',
  LOW = 'low',
}

 

이어서 CreateTodo의 내용을 수정하겠습니다.
priority 속성을 추가하고 @ApiProperty 데코레이터를 사용해 TodoPriorityenum을 지정해보겠습니다.

import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';

export class CreateTodoDto {
  @ApiProperty({
    maxLength: 20,
    description: 'Todo의 제목',
  })
  title: string;

  @ApiProperty({
    maxLength: 200,
    description: 'Todo의 설명',
  })
  description: string;

  @ApiProperty({
    description: 'Todo를 완성 했는지 여부',
  })
  completed: boolean;

  @ApiProperty({
    type: [String],
    description: 'Todo의 태그 목록',
  })
  tags: string[];

  @ApiProperty({
    enum: TodoPriority,
    description: 'Todo의 우선순위',
  })
  priority: TodoPriority;
}

http://127.0.0.1:3000/api에 접속해 Schemas 안의 CreateTodoDto를 확인해보면 아래와 같은 결과를 확인할 수 있습니다.

 

위의 결과로 Enum이 정상적으로 파싱되었습니다.

하지만 이를 Schema로 사용하고 싶다면 @ApiProperty 데코레이터에 enumName을 추가하면 됩니다.
아래는 예시입니다.

import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';

export class CreateTodoDto {
  @ApiProperty({
    maxLength: 20,
    description: 'Todo의 제목',
  })
  title: string;

  @ApiProperty({
    maxLength: 200,
    description: 'Todo의 설명',
  })
  description: string;

  @ApiProperty({
    description: 'Todo를 완성 했는지 여부',
  })
  completed: boolean;

  @ApiProperty({
    type: [String],
    description: 'Todo의 태그 목록',
  })
  tags: string[];

  @ApiProperty({
    enum: TodoPriority,
    enumName: 'TodoPriority', // Swagger에게 Schema를 만들라고 지정
    description: 'Todo의 우선순위',
  })
  priority: TodoPriority;
}

http://127.0.0.1:3000/api 에 접속한 후 Schemas를 클릭하면 아래와 같은 결과를 확인할 수 있습니다.

일부 복잡한 자료구조를 가진 데이터들은 어떻게 Swagger를 통해 해석할 수 있을까요? (ex: 2차원 배열)

DTO를 예로 들어보겠습니다. 

typearray로 지정하고 배열 타입을 사용하여 해당 배열 내부의 유형을 items 라고 지정해줍니다. string []

2차원 배열을 사용할 예정이기 때문에 다시 한번 배열로 지정해주어야 합니다. string[][]

여기서는 해당 2차원 배열에 사용되는 데이터 타입을 지정하기 위해 항목에 type을 구성하도록 하겠습니다.

 

위의 설명이 조금 추상적으로 느껴질수도 있기에 자세한 예제를 통해 설명하겠습니다.

CreateTodoDto 내에서 something 속성을 추가해 해당 타입을 string[][]으로 설정한 후
@ApiProperty 데코레이터를 적용하겠습니다.
그 다음 SwaggerModule이 해당 타입을 정상적으로 해석할 수 있도록 typeitems를 설정하겠습니다.

 

import { ApiProperty } from '@nestjs/swagger';
import { TodoPriority } from '../types/priority.type';

export class CreateTodoDto {
  @ApiProperty({
    maxLength: 20,
    description: 'Todo의 제목',
  })
  title: string;

  @ApiProperty({
    maxLength: 200,
    description: 'Todo의 설명',
  })
  description: string;

  @ApiProperty({
    description: 'Todo를 완성 했는지 여부',
  })
  completed: boolean;

  @ApiProperty({
    type: [String],
    description: 'Todo의 태그 목록',
  })
  tags: string[];

  @ApiProperty({
    enum: TodoPriority,
    enumName: 'TodoPriority', // Swagger에게 Schema를 만들라고 지정
    description: 'Todo의 우선순위',
  })
  priority: TodoPriority;
  //2차원 배열
  @ApiProperty({
    type: 'array',
    items: {
      type: 'array',
      items: {
        type: 'string',
      },
    },
  })
  something: string[][];
}

http://127.0.0.1:3000/api 에 접속해
Schema를 클릭 후 CreateTodoDto를 확인해보면 아래와 같은 결과를 확인할 수 있습니다.

 

 

마치며

Swagger는  적용하는 방법도 간단하고 유지보수도 쉽기 때문에 잘만 활용한다면 유용한 도구로 사용될 것 입니다.

문서화 시간을 획기적으로 줄여주고 프론트/백엔드단의 개발 효율도 증대시켜줍니다! 정말 추천할만한 라이브러리입니다.

다음 포스팅에서도 이어서 Swagger를 소개할 예정입니다.

자주 쓰이는 기능들을 자세히 소개해보도록 하겠습니다.

 

아래는 오늘 학습의 요약본입니다.

 

1. Swagger는 API를 시각화해 보여주는 도구입니다.

2. Nest는 Swagger를 래핑하여 모듈로써 제공해주고 있으며, 이름은 SwaggerModule입니다.

3. DocumentBuildercreateDocument로 기본 문서 양식을 생성할 수 있습니다.

4. setup을 사용해 Swagger UI를 만들 수 있습니다.

5. SwaggerModule은 API에 @Body, @Query, @Param과 같은 파라미터가 있는지를 감지하여
Swagger UI에 파라미터와 해당 자료형을 표시해줍니다.

6. DTO등 복잡한 자료유형의 경우에는 특별히 따로 처리를 해주어야 합니다. @ApiProperty @ApiBody 등..