내배캠 91일차

·2023년 2월 12일
0

내일배움캠프

목록 보기
99/142
post-thumbnail

게시판 만들기

board 관련 파일만들기

nest g mo board
nest g co board
nest g s board
// lodash 설치, 루트폴더에서
npm i lodash
  • tsconfig.json 속성 추가
    "esModuleInterop": true 추가
    ES6 모듈 사양을 준수하여 CommonJS 모듈을 가져올 수 있게 합니다.

board.controller.ts 1차

import { Controller, Delete, Get, Post, Put } from '@nestjs/common';
import { BoardService } from './board.service';

@Controller('board') // routing path is /board
export class BoardController {
  // 서비스 주입
  constructor(private readonly boardService: BoardService) {}

  // 게시물 목록을 가져오는 api
  @Get('articles')
  getArticles() {
    return this.boardService.getArticles();
  }

  // 게시물 상세보기 -> 게시물 ID로
  @Get('/articles/:id')
  getArticleById() {
    return this.boardService.getArticleById(id);
  }

  // 게시글 작성
  @Post('/articles')
  createArticle() {
    return this.boardService.createArticle();
  }

  // 게시물 수정
  @Put('/articles/:id')
  updateArticle() {
    return this.boardService.updateArticle(id);
  }

  // 게시물 삭제
  @Delete('/articles/:id')
  deleteArticle() {
    return this.boardService.deleteArticle(id);
  }
}

POST, PUT의 경우에는 클라이언트로부터 데이터를 전달받아야 게시물 생성을 하건, 수정을 하건 할 텐데 말이죠. Express에서는 req.body에 담긴 데이터를 인출해서 썼었는데 여기서는 어떻게 해야 할까요?

DTO

Nest.js에서는 클라이언트로부터 데이터를 받거나 데이터를 줘야 할 때는 DTO를 사용해야 합니다.

DTO에 대해서 간단하게 정의하자면 데이터를 전송하기 위해 작성된 객체라고 생각하시면 됩니다. Nest.js에서는 모든 데이터는 DTO를 통해서 운반되니 꼭 잊지 말아 주세요!
직접 DTO 객체를 작성해보기 전에, 우리는 class-validator, class-transformer라는 패키지를 설치를 할 거에요!
class-validator는 입력값 유효성 검사를 위해 정말 다양한 기능을 제공합니다.

npm i class-validator class-transformer

create-article.dto.ts

import { IsNumber, IsString } from 'class-validator';

export class CreateArticleDto {
  @IsString()
  readonly title: string;

  @IsString()
  readonly content: string;

  @IsNumber()
  readonly password: number;
}

update-article.dto.ts 1차

import { IsNumber, IsString } from 'class-validator';

export class UpdateArticleDto {
  @IsString()
  readonly title: string;

  @IsString()
  readonly content: string;

  @IsNumber()
  readonly password: number;
}

delete-article.dto.ts 1차

import { IsNumber } from 'class-validator';

export class DeleteArticleDto {
  @IsNumber()
  readonly password: number;
}

@nestjs/mapped-types

DTO를 3개 작성한 이유는 각각의 요청마다 해당 요청으로부터 전달을 받는 데이터가 조금씩 다 다르기 때문에 통상적으로 요청 1 : 1 DTO 비율을 유지합니다. 그런데, update-article.dto.ts는 create-article.dto.ts에서 사실상 다를게 없죠? 이럴 때는 코드 복사 + 붙여넣기도 좋지만 @nestjs/mapped-typesPartialType을 상속받으면 이를 깔끔하게 해결할 수 있습니다.

조금만 더 설명을 해보면 게시물을 수정할 때도 title, content, password를 동일하게 받아야 합니다. 이것을 PartialType을 상속받아서 해결하면 UpdateArticleDto는 CreateArticleDto 클래스의 부분집합이다. 라고 선언을 하게 되는 것이고 코드 복사 + 붙여넣기를 하지 않아도 동일한 효력을 가질 수 있습니다! 부분집합은 해당 필드가 전부 포함되어도 성립되고 특정 필드들이 생략이 되어도 성립이 되니까 문제가 없습니다!

그렇다면, DeleteArticleDto도 동일하게 할 수 있을까요? 네. 당연합니다! 하지만, 게시물을 삭제하는데 title, content를 굳이 선택적으로 받게끔 여지를 줄 필요는 없겠죠? 이럴 때는 더욱 더 단호하게 @nestjs/mapped-typesPickType을 상속받아 나는 이 부모 클래스에서 이 필드만 필요해!를 선언하는 것이 더욱 더 깔끔합니다.

npm i @nestjs/mapped-types

update-article.dto.ts 2차

import { PartialType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';

export class UpdateArticleDto extends PartialType(CreateArticleDto) {}

delete-article.dto.ts 2차

import { PickType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';

export class DeleteArticleDto extends PickType(CreateArticleDto, [
  'password',
] as const) {}

아예 새로운 유형의 DTO를 작성하는 것이 아니면 항상 @nestjs/mapped-types 사용을 습관화하여 생산성을 높여주세요!

또한, DTO의 유효성 검사를 하기 위해서는 main.ts에 ValidationPipe를 주입해야 해요.

DTO의 유효성 검사

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // 이 한줄만 넣어주면 됩니다! 잊지마세요!  
  await app.listen(3000);
}
bootstrap();

DTO를 작성하면 필요한 곳에서는 어디서든지 자유롭게 쓸 수 있고 유효성 검사 데코레이터는 DTO에서 한 번만 정의되면 됨!!

board.controller.ts 2차

dto를 사용해서 코드 수정!

  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateArticleDto } from './create-article.dto';
import { DeleteArticleDto } from './delete-article.dto';
import { UpdateArticleDto } from './update-article.dto';

@Controller('board') // routing path is /board
export class BoardController {
  // 서비스 주입
  constructor(private readonly boardService: BoardService) {}

  // 게시물 목록을 가져오는 api
  @Get('articles')
  getArticles() {
    return this.boardService.getArticles();
  }

  // 게시물 상세보기 -> 게시물 ID로
  @Get('/articles/:id')
  getArticleById(@Param('id') articleId: number) {
    return this.boardService.getArticleById(articleId);
  }

  // 게시글 작성
  @Post('/articles')
  createArticle(@Body() data: CreateArticleDto) {
    return this.boardService.createArticle(
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 수정
  @Put('/articles/:id')
  updateArticle(
    @Param('id') articleId: number,
    @Body() data: UpdateArticleDto,
  ) {
    return this.boardService.updateArticle(
      articleId,
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 삭제
  @Delete('/articles/:id')
  deleteArticle(
    @Param('id') articleId: number,
    @Body() data: DeleteArticleDto,
  ) {
    return this.boardService.deleteArticle(articleId, data.password);
  }
}
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateArticleDto } from './create-article.dto';
import { DeleteArticleDto } from './delete-article.dto';
import { UpdateArticleDto } from './update-article.dto';

@Controller('board') // routing path is /board
export class BoardController {
  // 서비스 주입
  constructor(private readonly boardService: BoardService) {}

  // 게시물 목록을 가져오는 api
  @Get('articles')
  getArticles() {
    return this.boardService.getArticles();
  }

  // 게시물 상세보기 -> 게시물 ID로
  @Get('/articles/:id')
  getArticleById(@Param('id') articleId: number) {
    return this.boardService.getArticleById(articleId);
  }

  // 게시글 작성
  @Post('/articles')
  createArticle(@Body() data: CreateArticleDto) {
    return this.boardService.createArticle(
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 수정
  @Put('/articles/:id')
  updateArticle(
    @Param('id') articleId: number,
    @Body() data: UpdateArticleDto,
  ) {
    return this.boardService.updateArticle(
      articleId,
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 삭제
  @Delete('/articles/:id')
  deleteArticle(
    @Param('id') articleId: number,
    @Body() data: DeleteArticleDto,
  ) {
    return this.boardService.deleteArticle(articleId, data.password);
  }
}

못보던 데코레이터인 @Param과 @Body가 생겼습니다. 여러분들이 생각하시는 그 파라미터와 바디가 맞습니다.

/articles/:id에서 :id는 파라미터를 뜻합니다. 그래서 저 URI에 지정된 파라미터를 가져오기 위해서는 @Param이라는 데코레이터에 갖고 올 파라미터 이름을 넘겨야합니다. 여기서는 id라는 파라미터를 갖고 오는 것이기 때문에 @Param(’id’)라고 갖고 온 후 number 타입의 articleId로 id라는 파라미터를 받겠다는 얘기죠!

바디를 갖고 오는 것은 @Body 데코레이터를 사용하면 됩니다. req.body로 전달되는 데이터를 UpdateArticleDto라는 타입의 객체의 data라는 변수로 받겠다는 의미입니다! 당연히, 잘못된 데이터가 있으면 ValidationPipe로 인해서 400 리턴이 자동으로 될 것입니다

board.service.ts

import {
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import _ from 'lodash';

@Injectable()
export class BoardService {
  // 데이터베이스를 사용하지 않아 일단은 배열로 구현을 하였으니 오해 말아주세요!
  // 보통은 TypeORM 모듈을 이용하여 리포지토리를 의존합니다. 이건 나중에 배울게요!
  private articles = [];

  // 게시글 비밀번호를 저장하기 위한 Map 객체입니다.
  private articlePasswords = new Map();

  getArticles() {
    return this.articles;
  }

  getArticleById(id: number) {
    return this.articles.find((article) => return article.id === id);
  }

  createArticle(title: string, content: string, password: number) {
    const articleId = this.articles.length + 1;
    this.articles.push({ id: articleId, title, content });
    this.articlePasswords.set(articleId, password);
    return articleId;
  }

  updateArticle(id: number, title: string, content: string, password: number) {
    if (this.articlePasswords.get(id) !== password) {
      throw new UnauthorizedException(
        `Article password is not correct. id: ${id}`,
      );
    }

    const article = this.getArticleById(id);
    if (_.isNil(article)) {
      throw new NotFoundException(`Article not found. id: ${id}`);
    }

    article.title = title;
    article.content = content;
  }

  deleteArticle(id: number, password: number) {
    if (this.articlePasswords.get(id) !== password) {
      throw new UnauthorizedException(
        `Article password is not correct. id: ${id}`,
      );
    }

    this.articles = this.articles.filter((article) => article.id !== id);
  }
}
profile
개발자 꿈나무

0개의 댓글