본문 바로가기
Back-end/기초부터 따라하는 nest.js

#12. 기초부터 따라하는 Nest.js : Dto와 Swagger

by hsloth 2023. 5. 17.

 

해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다.
모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다.

 

#pre. 터미널을 켜고 프로젝트 폴더로 이동

https://suloth.tistory.com/44

 

#0-1. 기초부터 따라하는 nest.js : 터미널 키는 법 + 터미널에서 작업 폴더 이동

윈도우 윈도우는 윈도우+R 버튼을 누른 후, cmd 를 입력하여 터미널을 킵니다. 혹은 윈도우 버튼을 눌러서 검색창에 cmd를 검색하면 터미널이 나올텐데 그걸 실행시켜주시면 됩니다. Mac OS Mac의 경

suloth.tistory.com

 
위의 링크의 내용을 참고하여 study 폴더로 이동해줍니다.
그리고 code . 명령어를 통해 vscode를 열어줍니다.


오늘은 DTO와 Swagger에 대해서 알아볼 예정입니다.

이전 포스팅에서는 간단한 CRUD를 작성해 보았습니다.

https://suloth.tistory.com/92

 

#11. 기초부터 따라하는 Nest.js : Article CRUD

해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다. 모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다. #pre. 터미널을 켜고 프로젝트 폴더로 이동 ht

suloth.tistory.com

 

타입스크립트에서 타입을 지정할 때, Typescript에 내재된 타입이 아닌, DTO라는 것을 만들어서 타입으로 지정해보겠습니다.

그리고, DTO를 이용해서 swagger까지 작성해보도록 하겠습니다.


DTO란?


Data Transfer Object의 약자로 계층간 데이터 교환을 위해 사용되는 객체입니다.

 

DTO라는 타입을 정의할 때, 해당 DTO에 넘겨주길 원하는 데이터 정보만 담아서 정의하면 불필요한 데이터를 교환하지 않을 수 있을 뿐만 아니라, 추가적으로 필요한 데이터의 정보도 정의할 수 있습니다.

그냥 간단히 말하면 DTO를 만든다는 건, 새로운 타입을 만드는 것이라고 할 수 있습니다.

DTO를 알기 전에는 함수의 인자로 들어가는 변수의 타입을 그냥 string으로 지정해주었다면, DTO를 사용한다면 { username: string, age: number } 와 같이 내가 사용하기 원하는 정보에 대해서만 타입을 지정할 수 있게 됩니다.

이렇게하면 vscode에서 자동완성 기능을 이용해서 해당 변수에 어떤 속성(객체)들이 있는지 쉽게 파악이 가능해지고, 필요한 속성을 미리 정해놓고 사용할 수 있다는 장점이 있습니다.

 


DTO 사용하기


DTO를 먼저 만들어봅시다.

먼저, 우리가 11강에서 만들었던 ArticleController의 createArticle 함수를 봐봅시다.

https://suloth.tistory.com/92

 

#11. 기초부터 따라하는 Nest.js : Article CRUD

해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다. 모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다. #pre. 터미널을 켜고 프로젝트 폴더로 이동 ht

suloth.tistory.com

 

  @UseGuards(JwtAuthGuard)
  @Post()
  async createArticle(@Body() body, @User() user) {
    const userId = user.id;

    const title = body.title;
    const content = body.content;

    const article = await this.articleService.createArticle(
      title,
      content,
      userId,
    );

    return article;
  }

body에 아무런 타입이 적용되어있지 않습니다. 이런식으로 body를 사용한다면, 코딩을 할 때 body안에 속성에 무엇이 있는지 파악하기 어려워 개발에 차질이 생길 것입니다.

 

const title = body.title;
const content = body.content;

위의 코드에서 body에 title이 들어있는지 없는지, content가 들어있는지 없는지 알 수 없습니다.

왜냐하면 body는 타입이 정해져있지 않기 때문이죠(정확히 말하면 any타입입니다)

 

그래서 DTO를 정의해서 body의 타입을 정의해보도록 하겠습니다.

 

Start

 

먼저, DTO를 정의하기 위한 패키지(라이브러리)를 설치해줍니다.

항상 말하지만, 프로젝트 최상위 폴더 (package.json이 존재하는 폴더)에서 npm install을 하셔야됩니다.

npm i @nestjs/swagger swagger-ui-express

 

 

src폴더 안에 dtos폴더를 만들어주고, dtos폴더 안에 article폴더를 또 만들어 줍시다. 그리고 article폴더 안에 create-article.dto.ts 파일을 만들어줍시다.

 

 

그리고 다음과 같이 코드를 작성해봅시다.

import { PickType } from "@nestjs/swagger";
import { ArticleEntity } from "src/entities/article.entity";

export class CreateArticleDto extends PickType(
  ArticleEntity, []
) {}

이 코드를 해석해보면

export class : 내보내기가 가능한(다른 파일에서 사용할 수 있는) 클래스를 정의할 건데

CreateArticleDto extends PickType : 그 클래스의 이름은 CreateArticleDto이고, PickType을 상속받고 있어

PickType(ArticleEntity, []) : 그리고 PickType은 ArticleEntity에서 []안에 들어가 있는 속성(칼럼)들만 Pick해서 가져오는 타입이야

라고 할 수 있습니다.

 

그리고 배열 [] 에서 따옴표를 입력하면 다음과 같이 ArticleEntity의 속성들이 자동완성리스트에 뜹니다.

 

 

여기서 저희는 title과 content만 필요하니, 그 두 개만 pick해봅시다.

그러면 다음과 같이 코드를 완성할 수 있습니다.

import { PickType } from '@nestjs/swagger';
import { ArticleEntity } from 'src/entities/article.entity';

export class CreateArticleDto extends PickType(ArticleEntity, [
  'title',
  'content',
]) {}

 

그 후, ArticleController로 가서 body의 타입을 CreateArticleDto로 설정해줍시다.

  @UseGuards(JwtAuthGuard)
  @Post()
  async createArticle(@Body() body: CreateArticleDto, @User() user) {
    const userId = user.id;

    const title = body.title;
    const content = body.content;

    const article = await this.articleService.createArticle(
      title,
      content,
      userId,
    );

    return article;
  }

 

그러면 이제 body를 사용할 때, 자동완성리스트에 title과 content가 있는 것을 볼 수 있습니다.

 

 

그러면, 이렇게 DTO를 정의함으로써 우리는 두 가지 이점을 얻을 수 있습니다.

1. body라는 불명확한 변수의 타입을 추론할 수 있게 되면서, 코딩할 때 body안에 어떤 값들이 담기는지 자동완성기능을 통해 바로바로 알 수 있다. (= 코딩 속도가 빨라지고, 오타가 발생할 확률이 줄어든다)

2. DTO에서 내가 사용하길 원하는 값들만 정의하여 값을 가져올 수 있다. (ArticleEntity의 title과 content)

 

body뿐만 아니라 다른 모든 타입을 DTO를 사용해서 정의하면 해당 변수, 함수에 담긴 값들을 조금 더 명확하게 알 수 있습니다.

 

여기까지 완료했다면, git을 통해 commit을 날려봅시다.

 

 


Swagger


자, 이제 Swagger를 사용해봅시다!

DTO를 만들 때 관련 패키지를 이미 설치했으니, 따로 또 설치할 필요는 없습니다.

 

Swagger란?

스웨거는 간단히 말하면, 자동으로 API 문서를 만들어주는 도구입니다.

백엔드 서버의 API에 대한 설명이 적혀있는 문서를 API 문서라고 하는데, 해당 API 문서를 프론트엔드 개발자들이 보면서 작업하게 됩니다.

이 API를 어떻게 호출하고, 호출한다면 어떤 결과값이 담기는 지 등등 에 대한 정보가 담겨 있습니다.

ex) POST /article 인 api는 게시글을 생성하는 api이고, body에 title과 content가 담겨있어야 하며, 로그인이 되어 있어야합니다. 그리고, 결과 값으로 생성된 게시글을 리턴합니다.

 

혹시 API가 뭔지 모르시겠다면, 그냥 Controller에 있는 함수라고 지금은 일단 생각합시다.

API를 호출한다 = 서버의 컨트롤러에 있는 함수를 실행한다

라고 일단 생각하시면 되겠습니다~

 

 

말로만 하면 어려우니 직접 해봅시다!

 

먼저, Swagger를 Nest.js 프로젝트에 추가해주어야 합니다. 그러기 위해서는 main.ts에서 Swagger를 설정해주면 됩니다.

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const options = new DocumentBuilder()
    .setTitle('API 문서 제목')
    .setDescription('API 문서 설명')
    .setVersion('0.0.1')
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api-docs', app, document); // 'api-docs'는 swagger문서로 접속할 url을 말한다.

  await app.listen(3000);
}
bootstrap();

여기서 중요한 점은 app.listen(3000)이 스웨거 설정보다 아래 있어야 한다는 겁니다.

 

그러면 이제 http://127.0.0.1:3000/api-docs 라는 url을 통해 스웨거 문서에 접속할 수 있습니다.

 

 

이제 Swagger관련 데코레이터들을 컨트롤러에 붙여줍시다.

ArticleController로 이동을 해서 createArticle 함수에 Swagger 데코레이터를 달아주겠습니다.

// article.controller.ts
...
import { CreateArticleDto } from 'src/dtos/article/create-article.dto';
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('게시글 API')     // ApiTag를 통해 Article Controller가 무슨 역할을 하는지 태그를 달아줍니다.
@Controller('article')
export class ArticleController {
  constructor(private readonly articleService: ArticleService) {}

  @ApiOperation({
    summary: '게시글 작성 API',
    description: '유저가 게시글을 작성한다.',
  })
  @ApiBody({
    type: CreateArticleDto,
  })
  @UseGuards(JwtAuthGuard)
  @Post()
  async createArticle(@Body() body: CreateArticleDto, @User() user) {
    const userId = user.id;

    const title = body.title;
    const content = body.content;

    const article = await this.articleService.createArticle(
      title,
      content,
      userId,
    );

    return article;
  }
  
  ...
  
}

@ApiTags() 를 통해 ArticleController가 어떤 역할을 하는지 태그를 달아줍니다.

@ApiOperation() 을 통해 createArticle이 어떻게 작동하는지 설명합니다.

@ApiBody() 를 통해 createArticle의 인자로 들어가는 Body값에 어떤 값이 들어가는지 설명합니다.

 

코드를 수정하고 다시 API 문서에 들어가보면,

 

 

이렇게 "게시글 API" 라는 태그가 생긴 것을 볼 수 있고, 그 하위 항목들에 article관련 api들이 있는 것을 볼 수 있습니다.

그리고 "게시글 작성 API" 라는 문구와 "유저가 게시글을 작성한다."라는 문구를 볼 수 있습니다.

 

그리고 Try out을 누르면

 

 

다음과 같이 body안에 데이터를 넣고, 해당 API를 실행시킬 수 있습니다.

하지만, 여기서 이상한 점이 하나 있습니다. 우리는 분명히 @ApiBody() 데코레이터에서 Body의 타입을 명시해주었는데, Swagger에는 반영이 되지 않았다는 점입니다. 

 

그러기 위해서는 ArticleEntity를 건드려야 합니다.

ArticleEntity로 가서 다음과 같이 코드를 작성해줍시다.

// entities/article.entity.ts
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { CommentEntity } from './comment.entity';
import { CommonBigPKEntity } from './common/common.entity';
import { UserEntity } from './user.entity';
import { ApiProperty } from '@nestjs/swagger';

@Entity('Article')
export class ArticleEntity extends CommonBigPKEntity {
  @ApiProperty({
    example: '게시글 제목입니다.',
    description: '게시글 제목',
    required: true,
  })
  @Column('varchar', { unique: false, nullable: false })
  title: string;

  @ApiProperty({
    example: '게시글 내용입니다.',
    description: '게시글 내용',
    required: true,
  })
  @Column('text', { unique: false, nullable: false })
  content: string;

  @Column('bigint', { unique: false, nullable: false })
  userId: string;

  @ManyToOne(() => UserEntity, (user) => user.articles)
  @JoinColumn({ name: 'userId', referencedColumnName: 'id' })
  user: UserEntity;

  @OneToMany(() => CommentEntity, (comment) => comment.article)
  comments: CommentEntity[];
}

다른 건 볼 필요없고, title과 content 위에 @ApiProperty() 데코레이터만 보면 됩니다.

코드를 이렇게 수정한 후, 서버를 재시작해서 swagger를 켜보면

 

 

 

다음과 같이 Body안에 값들이 미리 들어가 있는 것을 볼 수 있고,

Schema에 CreateArticleDto에 대한 설명이 적용되는 것을 볼 수 있습니다.

 


 

Swagger로 JWT 로그인


 

Swagger를 사용하기 전에! 로그인이 필요한 서비스가 있을 경우, 로그인을 해주어야 합니다.

이럴때는 어떻게 할까요?

 

먼저, 로그인 API를 손봐줍시다.

// auth.controller.ts

  @ApiOperation({
    summary: '로그인 API',
    description: '사용자가 로컬 로그인을 합니다.',
  })
  @ApiBody({})
  @UseGuards(LocalAuthGuard)
  @Post('login')
  async logIn(@Req() req) {
    const user = req?.user;

    console.log('user : ', user);

    return this.authService.logIn(user);
  }

이렇게만 코드를 작성해줍시다.

 

 

그 다음, main.ts 에서 swagger option에 addBearerAuth() 를 추가해줍시다.

// main.ts
  const options = new DocumentBuilder()
    .setTitle('API 문서 제목')
    .setDescription('API 문서 설명')
    .setVersion('0.0.1')
    .addBearerAuth()
    .build();

 

Swagger 문서를 새로고침해보면, 다음과 같이 Authorize 아이콘이 생깁니다.

로그인 로직을 통해 토큰을 발급 받으면, 이곳을 통해 넣어주면 됩니다.

 

 

그리고 로그인이 필요한 API라는 것을 표현하기 위해 컨트롤러에 ApiBearerAuth() 데코레이터를 붙여줍니다.

일단 createArticle에만 붙여보겠습니다.

코드를 다음과 같이 수정해줍시다.

// article.controller.ts
  @ApiOperation({
    summary: '게시글 작성 API',
    description: '유저가 게시글을 작성한다.',
  })
  @ApiBody({
    type: CreateArticleDto,
  })
  @ApiBearerAuth()  // 추가된 코드
  @UseGuards(JwtAuthGuard)
  @Post()
  async createArticle(@Body() body: CreateArticleDto, @User() user) {
    const userId = user.id;

    const title = body.title;
    const content = body.content;

    const article = await this.articleService.createArticle(
      title,
      content,
      userId,
    );

    return article;
  }

 

코드를 저장하고 스웨거에서 새로고침을 누르면, 게시글 작성 API에 자물쇠 표시가 뜨게 됩니다.

 

자, 이제 게시글을 생성해봅시다.

먼저, 로그인이 안 된 상태에서 게시글 작성 API를 실행시켜 보겠습니다.

 

다음과 같이 body의 title과 content를 설정해주고 Execute 버튼을 눌러봅시다.

 

 

Unauthorized 에러가 발생합니다. 

 

그러면, 로그인을 해봅시다.

 

스웨거 문서에 들어가서 다음과 같이 로그인을 하면

 

위 사진 처럼 accessToken이 발급됩니다.

 

이제, 이 accessToken을 복사해줍시다.

 

 

그리고 맨 위에 Authorize 아이콘을 클릭하고, value 부분에 해당 토큰을 붙여넣기 합니다.

 

그리고 Authorize 버튼을 눌러줍시다.

 

그러면, 이제 Article을 생성해봅시다.

 

다음과 같이 게시글 작성 API의 body를 설정하고 Execute 버튼을 누르면

 

이렇게 게시글이 작성되는 것을 볼 수 있습니다.

 

그러면! 이제 커밋을 해봅시다.

https://suloth.tistory.com/46

 

#0-2. 기초부터 따라하는 Nest.js : Git과 Github 사용법

이 글은 아래의 포스팅에 이어서 작성하는 포스팅입니다. https://suloth.tistory.com/45 #1. 기초부터 따라하는 Nest.js : Nest.js 초기 설정 Nest.js란? node.js의 백엔드 프레임워크 중 하나입니다. node.js에는 수

suloth.tistory.com

 

 


Github에 Push


자, 여기까지 다 마무리 하셨다면 깃 허브에 푸쉬 해봅시다.

 

https://suloth.tistory.com/46

 

#0-2. 기초부터 따라하는 Nest.js : Git과 Github 사용법

이 글은 아래의 포스팅에 이어서 작성하는 포스팅입니다. https://suloth.tistory.com/45 #1. 기초부터 따라하는 Nest.js : Nest.js 초기 설정 Nest.js란? node.js의 백엔드 프레임워크 중 하나입니다. node.js에는 수

suloth.tistory.com

 

참고하시라고 올려두는 사진입니다.