우당탕탕 우리네 개발생활

[Nestjs] validator 적용해보기(feat. mongoId) 본문

tech

[Nestjs] validator 적용해보기(feat. mongoId)

미스터카멜레온 2023. 11. 11. 22:25

백엔드 서버를 구성하면서 중요하게 생각하고 있는 것 중 하나는 정교한 validation입니다.

 

보통 저는 서버를 구축한 이후 API 문서를 작성합니다. 각 API 마다 Request에 필요한 파라미터들의 타입명시를 잘해놓고 테스트도 곧잘 합니다.(API문서에 명시한 스펙으로 말이죠.) 하지만 온갖 다양한 타입의 파라미터들을 Request에 담아 테스트를 하다 보면 예기치 않은 에러들을 마주하는 상황이 종종 생깁니다. 예기치 않은 에러라는 것은 문서에 명시하지 않은 에러코드를 나타낸다는 뜻이고 이는 해당 API를 사용하는 클라이언트 입장에서 핸들링할 수 없게 됩니다. 이로 인해 생길 수 있는 피해가 적다고 해도 이는 치명적인 상황으로 여기는 게 맞다고 생각합니다. 이런 생각으로 저는 validation적용과 테스트에 각별히 신경을 쓰고 있습니다.

 

Expressjs를 사용하면서 zod라는 라이브러리를 애용했습니다. 사용한기간은 그리 길지 않았지만 러닝커브가 완만하고 validator라는 역할을 하는데 부족함이 없어 잘 사용했던 기억이 있습니다. Nestjs를 공부하기 시작하면서도 zod를 떠올렸으나 공식홈페이지에서 권장하는  validation을 위한 기능들이 있음을 알게 되었습니다.(아래 사이트를 통해 공부하면서 처음으로 접하게 되었습니다. 너무 좋은 사이트입니다.) 웬만해서는 가이드라인을 지키며 개발을 해보고 싶은 마음이라 이를 이용하여 아래 프로젝트에 적용을 해봤습니다. 이용했던 경험을 통해 validator의 적용방법을 공유하고자 합니다.

https://codegear.tistory.com/

 

코드 기어 Code Gear - 개발 강좌

프로그램 개발 관련 유튜브 강의 자료 블로그

codegear.tistory.com

 

설명에서 예시로 들게 될 코드들은 모두 아래 프로젝트에 있습니다.

https://github.com/mmmmicha/newsfeed.api

 

GitHub - mmmmicha/newsfeed.api: nestjs + mongoose practice

nestjs + mongoose practice. Contribute to mmmmicha/newsfeed.api development by creating an account on GitHub.

github.com

 

1. 라이브러리 설치

validatior를 적용하기 위해서는 아래와 같은 라이브러리들이 필요합니다.

  • class-validator
  • class-transformer

아래 명령어를 통해 설치해줍니다.

npm i --save class-validator class-transformer

 

2. DTO에 class-validator적용

사용법은 굉장히 간단합니다. 아래 링크에 있는 공식사이트에서 필요한 사용법을 찾아 용도에 맞게 적용하시면 되겠습니다.

https://www.npmjs.com/package/class-validator

 

class-validator

Decorator-based property validation for classes.. Latest version: 0.14.0, last published: a year ago. Start using class-validator in your project by running `npm i class-validator`. There are 5000 other projects in the npm registry using class-validator.

www.npmjs.com

 

아래 코드는 class-validator를 이용한 DTO의 예시입니다. 아래 예시를 작성하면서 얻은 여러 경험들을 잠깐 공유하겠습니다.

 

첫 번째,타입스크립트를 통한 타입명시는 validator에 영향을 미치지 않습니다.

그렇기 때문에 예를 들어 String이라는 타입을 엄격하게 체킹 하기 위해서는 @IsString()이라는 데코레이터를 사용하셔야 합니다. 다른 타입들도 마찬가지입니다. title이라는 필드에 string이라는 타입명시를 타입스크립트를 통해 해 놨음에도 불구하고 @IsString()이라는 데코레이터를 추가한 것을 보실 수 있습니다. 

 

두 번째, @IsNotEmpty()라는 데코레이터는 비어있는 스트링을 걸러낼 수 없습니다. 

그렇기 때문에 엄격하게 필드를 정의하기 위해서는 length에 관련된 데코레이터를 추가하는 방법이 보완책이 될 수 있습니다.

 

세 번째, @IsMongoId()라는 데코레이터를 통해 mongoDB의 id를 검증할 수 있습니다.

이 데코레이터의 존재유무를 몰랐을 때, 우선 String으로 파라미터를 받은 후 Service 내에서 mongoose.Types.IsObject.isValid()라는 함수를 통해 검증을 했었습니다. 하지만 아무리 생각해도 파라미터의 검증이 통일된 방법으로 이뤄지지 않는 점과 Controller를 통과하여 Service에 영향을 미친다는 점이 불편했습니다. 그 결과 위 방법을 발견하게 되었습니다. Nestjs+mongoose의 조합으로 개발하시는 분들이라면 유용하게 사용하실 수 있겠습니다.

// createNews.dto.ts

import { IsMongoId, IsNotEmpty, IsOptional, IsString } from "class-validator";

export class CreateNewsDTO {
    @IsNotEmpty()
    @IsString()
    title: string;
    @IsNotEmpty()
    @IsString()
    content: string;
    @IsNotEmpty()
    @IsString()
    @IsMongoId()
    pageId: string;
    @IsOptional()
    @IsString()
    @IsMongoId()
    ownerId: string;
}

 

3. Controller에 ValidationPipe적용

앞서 class-validator를 이용하여 DTO를 작성해 봤습니다. 아마도 많은 분들이 DTO를 request body에 담겨오는 데이터들을 Controller에서 받는 데 사용을 하실 거라고 생각합니다. Nestjs에서는 class-validator의 데코레이터들을 인식하기 위해 ValidationPipe라는 클래스를 제공합니다. ValidationPipe를 사용하기 위해선 @UsePipes()라는 데코레이터를 반드시 사용해야 합니다. 아래코드를 통해 예시를 확인하실 수 있습니다.

 

맨 처음 ValidationPipe를 사용했을 때, 가장 기본적인 방법으로 @UsePipes(ValidationPipe) 조합을 사용했었습니다. 이렇게 설정을 했을 때의 문제점은, DTO에 명시하지 않은 파라미터가 들어오게 되었을 경우 이를 거르지 않고 허용해 버린다는 점이었습니다. 이 부분을 해결하기 위해 아래 두 가지 옵션을 찾게 되었습니다.

  • whitelist: true
  • forbidNonWhitelisted: true

whitelist는 어떠한 class-validator데코레이터도 붙지 않은 필드는 매핑 대상에서 제외시켜 버리는 특성입니다. 즉, DTO 내 데코레이터를 통해 명시하지 않은 필드는 무시하게 됩니다. 이 자체로도 이미 제가 원하는 기능을 충족했었지만 forbidNonWhitelisted라는 옵션을 통해 보강했습니다. forbidNonWhitelisted는 반드시 whitelist와 함께 사용해야 하는데, 이는 데코레이터가 사용되지 않은 파라미터를 허용하지 않고 exception을 발생시켜 버립니다.(공식사이트의 설명이 부족하여 chatGPT에 질문해 본 결과 forbidNonWhitelisted라는 옵션만 단독으로 사용하게 되면 명시하지 않은 파라미터들을 허용한다고 하니 유의하셔서 사용하셔야겠습니다.)

 

ValidationPipe 역시 다양한 추가옵션들이 있기 때문에 공식사이트를 확인해 보시길 권장드리겠습니다.

https://docs.nestjs.com/techniques/validation

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

// news.controller.ts

import { Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { NewsService } from './news.service';
import { AuthGuard } from '../auth/security/auth.guard';
import { RolesGuard } from '../auth/security/roles.guard';
import { Roles } from '../auth/decorator/role.decorator';
import { RoleType } from '../auth/role-type';
import { CreateNewsDTO } from './dto/createNews.dto';
import { Request, Response } from 'express';
import { UpdateNewsDTO } from './dto/updateNews.dto';

@Controller('news')
export class NewsController {
    constructor(private readonly newsService: NewsService) {}

    @Post()
    @UseGuards(AuthGuard, RolesGuard)
    @Roles(RoleType.TEACHER)
    @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
    async create(@Body() createNewsDTO: CreateNewsDTO, @Req() req: Request, @Res() res: Response): Promise<any> {
        const userId = req.user['_id'];
        createNewsDTO.ownerId = userId;
        const payload = await this.newsService.create(createNewsDTO);
        return res.send({ message: 'ok', payload: payload });
    }
    
...

}

 

Nestjs validation을 적용해 본 경험을 공유드려봤습니다. Nestjs에 좋은 기능들이 많이 있지만 validation을 적용하면서 겪은 다양한 경험들이 유독 많았습니다. 저도 아직 지식이 많이 부족한 상태이기에 다양한 부분들을 적용해 보며 발전해나가려고 합니다. 제 경험이 도움이 되셨길 바라겠습니다.

 

감사합니다.