2023. 6. 3. 16:34ㆍ개발 문서 번역/NestJS
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.
전 포스팅에서 객체 양식에 있는 자료들을 어떻게 검증해야 하는지에 대해 언급한 적이 있습니다.
DTO, ValidationPipe, class-validator와 class-transformer를 사용하기만 하면 이 문제를 쉽게 해결할 수 있습니다.
먼저 npm을 통해 class-validator와 class-transformer를 설치하도록 하겠습니다.
$ npm install --save class-validator class-transformer
DTO 양식 검증
이 검증 로직을 실험해보기 위해 TodoModule과 TodoController를 생성 하겠습니다.
$ nest generate module features/todo
$ nest generate controller features/todo
이어서 feature/todo 아래 dto 폴더에 create-todo.dto.ts를 생성해줍니다.
검증에는 class의 형식으로 DTO를 반드시 생성해주어야 합니다.
이유는 Controller(하) 편에서 언급한 적이 있습니다.
interface의 형식으로 작성된다면 JavaScript로 컴파일되는 과정에서 삭제됩니다.
이는 Nest에서도 DTO의 양식이 어떤것인질 알지 못하게 됩니다.
아래는 create-todo-dto.ts의 예제입니다.
이 포스팅에서 원하는 title의 양식입니다.
1. 필수 입력
2. String으로만 값을 받고싶음
3. 길이는 최대 20자
description의 양식은..
1. 선택 입력*
2. String으로만 값을 받고싶음
그렇다면 이 검증을 어떻게 적용해야 할까요? class-validator를 사용하면 바로 적용할 수 있습니다.
해당 자료형의 속성을 특정한 데코레이터들을 추가하면 됩니다.
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
@IsString()
@IsOptional()
public readonly description?: string;
}
주의: 자세한 데코레이터는 class-validator 를 참고해주세요.
이렇게 하면 해당 규칙들을 정의할 수 있습니다. 정말 편하지 않나요?
다음으로 해당 자원에 @UsePipes 데코레이터에 ValidationPipe를 추가하면 됩니다.
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(ValidationPipe)
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
Controller 레이어에서 적용시켜주어도 됩니다.
이렇게되면
해당 Controller에서 받아지는 모든 요청에서 자원의 검증을 진행하게 됩니다.
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
@UsePipes(ValidationPipe)
export class TodoController {
@Post()
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
Postman으로 테스트 해보면 이제 title에 대한 오류 메세지가 표시됩니다.
오류 메세지 항목 끄기
이 오류 메세지의 항목을 원하지 않을 수도 있습니다. ValidationPipe를 통해 disableErrorMessages를 통해 끌 수 있습니다.
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ disableErrorMessages: true }))
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
Postman을 통해 테스트 해봅시다.
커스텀 Exception (Custome Exception, 自訂 Exception)
Pipe와 똑같이 exceptionFactory를 통해 커스텀 Exception을 정의할 수 있습니다.
import { Body, Controller, HttpStatus, NotAcceptableException, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { ValidationError } from 'class-validator';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(
new ValidationPipe({
exceptionFactory: (errors: ValidationError[]) => {
return new NotAcceptableException({
code: HttpStatus.NOT_ACCEPTABLE,
message: '올바른 양식이 아닙니다.',
errors
});
}
})
)
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
Postman을 통해 테스트한 결과입니다.
자동 필터링 속성
Todo를 예로 들면 title과 description만을 파라미터로써 받을 때
프론트단에서의 요청이 아래와 같이 들어왔다고 가정 하겠습니다.
{
"title": "Test",
"text": "Hello."
}
아예 관련없는 text가 전송되었습니다. 이때 빠르게 이런 파라미터들을 필터링 하는 방법은 어떻게 해야할까요?
ValidationPipe에 whitelist를 설정하면 됩니다.
이 whitelist가 true일 때 DTO에 정의되지 않은 속성들은 전부 자동으로 필터링됩니다.
이는 class-validator에 정의된 속성이 같이 요청에 전송되더라도,
정의되지 않은 속성이 있다면 해당 속성을 무효로 합니다.
아래는 whitelist의 예제입니다.
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
아래는 Postman로 테스트한 결과입니다.
유효하지 않은 파라미터가 전송되었을 때 바로 틀렸다는 응답을 보낼 경우
whitelist와 forbidNonWhitelisted를 동시에 사용하면 됩니다!
자동 변환
ValidationPipe는 transform 파라미터를 통한 전달된 객체를 DTO의 인스턴스로 변환하는 기능도 제공합니다.
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
create(@Body() dto: CreateTodoDto) {
console.log(dto);
return dto;
}
}
Postman을 통해 테스트를 하고나면 터미널에 아래와 같은 결과가 찍히는데
dto가 CreateTodoDto의 인스턴스임을 확인할 수 있습니다.
CreateTodoDto { title: 'Test' }
transform은 하나 더 엄청난 기능을 제공합니다. 어떻게 라우터 파라미터의 값을 얻어올 수 있는지 기억 나시나요?
우리가 id 라우터 파라미터를 얻어오고 싶고, id 자료형은 number입니다. 하지만 일반적인 상황이라면 라우터 파라미터를 받아올 때에는 string일것입니다. transform을 통해 Nest는 우리가 지정한 값으로 변환하여 사용할 수 있게됩니다.
import { Controller, Get, Param, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('todos')
export class TodoController {
@Get(':id')
@UsePipes(new ValidationPipe({ transform: true }))
get(@Param('id')id : number) {
console.log(typeof id);
return '';
}
}
http://127.0.0.1:3000/1에 접속하면 터미널에는 이미 number로 변경된걸 확인할 수 있습니다.
number
배열 형식 DTO 검증하기
배열 형식으로 전달되는 DTO는 ValidationPipe을 사용할 수 없습니다.
ParseArrayPipe를 사용해야 하며, items를 해당 DTO에 넣어주면 됩니다.
import { Body, Controller, ParseArrayPipe, Post } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
create(
@Body(new ParseArrayPipe({ items: CreateTodoDto }))
dtos: CreateTodoDto[]
) {
return dtos;
}
}
Postman으로 다음과 같이 배열 형식으로 전송하면, 아래와 같은 결과가 출력됩니다.
쿼리 파라미터 해석
ParseArrayPipe는 쿼리 파라미터를 해석할 수 있습니다. ?ids=1,2,3을 검색하고자 할 경우
해당 방법을 사용하여 id를 해석하면 됩니다. separator를 추가하여 어떤게 구분점인지를 명시합니다.
import { Controller, Get, ParseArrayPipe, Query } from '@nestjs/common';
@Controller('todos')
export class TodoController {
@Get()
get(
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[]
) {
return ids;
}
}
Postman을 통해 테스트하면..
DTO 사용 팁
시스템이 커지면 커질수록 DTO의 갯수도 따라서 증가하게 됩니다.
이때 많은 속성들이 중복될 수 있습니다. ex: 같은 Resource의 CRUD DTO
이는 유지보수의 어려움으로 이어지는데 Nest는 이에 대해 명쾌한 해답을 제공합니다.
특수한 상속 방법으로 처리되며 이에대해 천천히 소개해보겠습니다.
모든 값을 선택사항으로 변경하여 사용하기 (Partial)
이 옵션은 DTO의 모든 필드값을 가져와 사용합니다.
단지 이 옵션은 모든 항목을 선택사항으로 변경하여 가져오며,
이 PartialType 함수에 DTO를 넣어주는 방법으로 사용할 수 있습니다. 이때 새로운 DTO를 상속받습니다.
먼저 이 라이브러리를 설치 후에..
$ npm install @nestjs/mapped-types
아래는 update-todo.dto.ts의 예제를 dto 디렉토리에 생성하여 CreateTodoDto의 필드값을 받아온 예제입니다.
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PartialType(CreateTodoDto) {
}
위의 코드는 아래와 같은 의미입니다.
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
@IsOptional()
public readonly title?: string;
@IsString()
@IsOptional()
public readonly description?: string;
}
이어서 todo.controller.ts를 수정하겠습니다.
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
Postman에서 테스트를 해봅시다. PATCH /todos/:id 를 적어 요청을 보내보면...
해당 검증을 통과하는 모습을 확인할 수 있습니다.
선택적으로 사용하기 (Pick)
선택적으로 사용한다는 의미는 DTO에서 우리가 선택하고 싶은 값만 선택하여 가져올수 있다는 의미입니다.
PickType라는 함수를 통해 안에 해당 DTO에서 사용할 값을 정의 해주는 방법으로 사용할 수 있습니다.
이때 새로운 DTO를 상속받습니다.
UpdateTodoDto에 다시 CreateTodoDto의 title의 필드 값을 상속받아보겠습니다.
import { PickType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PickType(CreateTodoDto, ['title']) {
}
위의 코드는 아래의 코드와 동일한 효과입니다.
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
}
todo.controller.ts는 똑같이 앞에서의 예제를 사용하겠습니다.
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
Postman으로 똑같이 어떠한 값도 넣지 않고 테스트를 진행해보겠습니다.
검증에 통과하지 못한 결과를 확인할 수 있습니다.
원하지 않는 값 빼고 사용하기 (Omit)
원하지 않는 값 빼고 사용하기는 DTO를 사용할 때 필요 없는 속성을 명시해주어 빼고 사용할수 있는 방법입니다.
OmitType 함수를 통해 원하는 DTO에 사용하지 않을 값을 명시해주어 사용할 수 있습니다.
이때 새로운 DTO를 상속받습니다.
UpdateTodo를 이어서 수정하여 진행하겠습니다. 이번 예제에서는 title을 사용하지 않겠다고 명시하겠습니다.
import { OmitType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends OmitType(CreateTodoDto, ['title']) {
}
해당 코드는 아래와 같습니다.
import { IsOptional, IsString } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@IsOptional()
public readonly description?: string;
}
todo.controller.ts를 조금 수정하여 whitelist와 forbidNonWhitelisted를 true로 바꾸어 테스트해보겠습니다.
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
Postman을 통해 테스트를 진행해보면
title을 입력하고 PATCH /todos/:id에 요청을 보냈습니다.
하지만 whitelist, forbidNonWhitelisted를 설정했기 때문에 검증에는 통과하지 못했습니다.
합쳐 사용 (Intersection)
합쳐 사용한다는 의미는 두개의 DTO를 합치는 것을 의미합니다.
IntersectionType이 사용되며 이 함수에 합치고자 하는 2개의 DTO를 입력하면 됩니다.
이때 새로운 DTO가 상속됩니다.
똑같이 CreateTodoDto와 update-todo.dto.ts를 사용하는데 MockDto를 하나 정의하여 UpdateTodoDto가 2개의 DTO 필드 값을 상속받도록 하겠습니다.
import { IntersectionType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsString } from 'class-validator';
import { CreateTodoDto } from './create-todo.dto';
export class MockDto {
@IsString()
@IsNotEmpty()
public readonly information: string;
}
export class UpdateTodoDto extends IntersectionType(CreateTodoDto, MockDto) {
}
위의 코드는 아래와 같은 의미입니다.
todo.controller.ts를 수정해보겠습니다.
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
@IsString()
@IsOptional()
public readonly description?: string;
@IsString()
@IsNotEmpty()
public readonly information: string;
}
Postman을 통해 테스트를 진행하면 일부러 information을 넣지 않고 PATCH /todos/:id 에 요청을 넣어보면..
검증을 통과하지 못하는걸 확인할 수 있습니다.
조합해서 사용
위에서 언급한 4개의 함수 PartialType, PickType, OmitType, IntersectionType을 조합하여 사용할수도 있습니다.
아래는 OmitType을 이용해 CreateTodoDto의 title 필드를 없애고 IntersectionType으로 MockDto를 합쳐 사용하겠습니다.
import { IntersectionType, OmitType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsString } from 'class-validator';
import { CreateTodoDto } from './create-todo.dto';
export class MockDto {
@IsString()
@IsNotEmpty()
public readonly information: string;
}
export class UpdateTodoDto extends IntersectionType(
OmitType(CreateTodoDto, ['title']), MockDto
) {
}
위는 아래의 코드와 동일한 의미입니다.
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@IsOptional()
public readonly description?: string;
@IsString()
@IsNotEmpty()
public readonly information: string;
}
todo.controller.ts 는 그대로 두고 진행하겠습니다.
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
Postman을 통해 테스트를 진행해보면 PATCH /todos/:id에 요청을 해보면 검증을 통과할 수 없습니다.
전역 Pipe
ValidationPipe는 엄청 자주 쓰이는 기능중에 하나입니다.
대부분의 경우에는 DTO의 데이터 유효성을 확인하는데 사용됩니다.
따라서 ValidationPipe를 전역으로 구성하여 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();
useGlobalPipes를 사용해 ValidationPipe를 전역으로 적용하면 끝납니다. 간단하죠?
의존성 주입을 통해 전역 Pipe 사용하기
위의 방법은 모듈 외부에서 설정했었습니다. 하지만 또 다른 방법이 있는데요
Exception Filter와 같이 의존성 주입을 통해 사용할 수 있습니다.
이를 위해 Provider의 token을 APP_PIPE로 구현하면 되는데요. 아래는 useClass로 class를 인스턴스화 시킨 예제입니다.
import { Module, ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useClass: ValidationPipe
}
],
})
export class AppModule {}
마치며
ValidationPipe와 DTO는 검증에 있어 무척 중요한 역할입니다.
어떠한 API도 자료의 유효성 검사는 꼭 진행되어야 하며 이를 통해 의도치 못한 상황을 줄일 수 있습니다.
오늘 배운것들을 간단히 요약하며 포스팅을 마치겠습니다.
1. ValidationPipe는 class-validator와 class-transformer를 설치하여야 합니다.
2. ValidationPipe를 통해 DTO의 양식을 검증할 수 있습니다.
3. ValidationPipe은 disableErrorMessage을 통해 오류 메세지를 끌 수 있습니다.
4. ValidationPipe은 exceptionFactory를 통해 Exception을 커스텀할 수 있습니다.
5. ValidationPipe은 whitelist를 통해 유효하지 않은 파라미터를 필터링 할 수 있습니다.
유효하지 않은 파라미터에 오류 응답을 하고싶은 경우 따로 forbidNonWhitelisted를 입력해주어야 합니다.
6. ValidationPipe는 transform을 통해 자동으로 자료형 변환을 할 수 있습니다.
7. ParseArrayPipe를 통해 배열 형태의 DTO와 쿼리 파라미터를 해석할 수 있습니다.
8. DTO는 PartialType, PickType, OmitType, IntersectionType 이 네개의 함수를 통해 DTO의 필드값을 재사용 할 수 있습니다.
9. 전역 Pipe를 의존성 주입을 통해 구현할 수 있습니다.
'개발 문서 번역 > NestJS' 카테고리의 다른 글
NestJS 帶你飛! 시리즈 번역 12# Interceptor (0) | 2023.06.05 |
---|---|
NestJS 帶你飛! 시리즈 번역 11# Middleware (0) | 2023.06.04 |
NestJS 帶你飛! 시리즈 번역 09# Pipe (상) (0) | 2023.06.02 |
NestJS 帶你飛! 시리즈 번역 08# Exception & Exception filters (0) | 2023.06.01 |
NestJS 帶你飛! 시리즈 번역 07# Provider (하) (0) | 2023.05.31 |