HooneyLog

Nest.js에서 dto를 사용하여 validation과 데이터 변형 시켜보기

프로필 이미지

Seunghoon Shin

2022년 5월 22일 01:57

이번 글에서는 타입스크립트 백엔드 프레임워크인 Nest.js에서 POST나 PUT과 데이터 요청이 들어왔을때 그 데이터가 적절한 데이터인지 확인하는 validation

GET을 했을때 서버 데이터에서 가지고 있는 raw데이터 형식이 아니라 중간에서 Interceptor하여 다른 형태의 데이터로 변환 시켜 줄 수 있는 transform을 한번 해보려고 한다.

먼저 테스트 하기 위한 데이터를 ts파일에서 넣는다.

Test Data

export type ReportType = 'income' | 'expense';

export interface Reports {
  reports: {
    id: string;
    source: string;
    amount: number;
    created_at: Date;
    updated_at: Date;
    type: ReportType;
  }[];
}

export const data: Reports = {
  reports: [
    {
      id: 'uuid',
      source: 'Salary',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'income',
    },
    {
      id: 'uuid232',
      source: 'Salary',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'income',
    },
    {
      id: 'uuid2',
      source: 'Youtube',
      amount: 300,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'expense',
    },
    {
      id: 'uuid3',
      source: 'Facebook',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'expense',
    },
  ],
};

데이터 구조는 아래와 같이 이루어져있고, POST를 통해 데이터를 삽입하는 것으로 예시를 들어보겠다.

Controller

@Post()
  createReport(@Param('type') pType: ReportType, @Body() body: Report) {
    return this.appService.createReport(pType, body);
  }
  • pType이라는 매개변수로 클라이언트에서 보낸 type 값이 들어온다.
  • 위와 마찬가지로 body안에도 클라이언트가 보낸 body 객체가 들어온다.
  • Service

    createReport(pType: ReportType, body: Report) {
        if (!isDataType(pType)) {
          return { message: '잘못된 타입' };
        }
    
        const newReport: Reports['reports'][number] = {
          id: uuid(),
          created_at: new Date(),
          updated_at: new Date(),
          type: pType,
          ...body,
        };
    
        data.reports.push(newReport);
        return data;
      }
  • 컨트롤러에서 들어온 데이터를 받아 비즈니스 로직을 처리하고 return하여 controller에 보내준다
  • controller는 이 데이터를 받아 클라이언트에 보여준다
  • 문제점

    위와 같은 형식으로 컨트롤러를 처리하면 아직 validation이 안되는 상태이기때문에 잘못된 데이터가 들어와도 에러가 발생하지 않고 컨트롤러 진입하게된다.

    빈 source와 음수인 amount 데이터를 요청

    위 요청에 따른 응답값 (맨 아래 보면 잘못된 데이터가 들어가 리턴된 모습을 볼 수 있다 )

    {
        "reports": [
            {
                "id": "uuid",
                "source": "Salary",
                "amount": 7500,
                "created_at": "2022-05-22T02:07:35.647Z",
                "updated_at": "2022-05-22T02:07:35.647Z",
                "type": "income"
            },
            {
                "id": "uuid232",
                "source": "Salary",
                "amount": 7500,
                "created_at": "2022-05-22T02:07:35.647Z",
                "updated_at": "2022-05-22T02:07:35.647Z",
                "type": "income"
            },
            {
                "id": "uuid2",
                "source": "Youtube",
                "amount": 300,
                "created_at": "2022-05-22T02:07:35.647Z",
                "updated_at": "2022-05-22T02:07:35.647Z",
                "type": "expense"
            },
            {
                "id": "uuid3",
                "source": "Facebook",
                "amount": 7500,
                "created_at": "2022-05-22T02:07:35.647Z",
                "updated_at": "2022-05-22T02:07:35.647Z",
                "type": "expense"
            },
            {
                "id": "d283f562-7598-41ab-8321-4a3ce28c0b7b",
                "created_at": "2022-05-22T02:07:40.157Z",
                "updated_at": "2022-05-22T02:07:40.157Z",
                "type": "income",
                "amount": -1,
                "source": ""
            }
        ]
    }

    Dto를 사용하여 Validation 하기

    만약 오로지 양수인 amount와 반드시 입력된 source가 있는 데이터를 받고 그 외 데이터가 들어오면 오류를 발생시키려면 어떻게 해야할까?

    src → dto → report.dto 생성 후 아래와 같이 입력

    import {
      IsNotEmpty,
      IsNumber,
      IsPositive,
      IsString,
    } from 'class-validator';
    
    export class CreateReportDto {
      @IsNumber()
      @IsPositive()
      amount: number;
    
      @IsString()
      @IsNotEmpty()
      source: string;
    }
  • CreateReportDto 라는 클래스를 만든다
  • class-validator 모듈 안에 들어있는 validate를 하기위한 데코레이터들을 import 한다
  • 원하는 validation을 해준다
  • IsNumber → 오직 숫자만

    IsPositive → 오직 양수만

    IsString → 오직 문자만

    IsNotEmpty → 무조건 값이 필요

    해당 Dto를 컨트롤러에 적용

    body타입에 해당 Dto 인스턴스 타입을 넣어준다

    @Post()
      createReport(
        @Param('type') pType: ReportType,
        @Body() body: CreateReportDto,
      ) {
        return this.appService.createReport(pType, body);
      }
    성공적인 validation ( 데코레이터로 지정하지 않은 데이터가 들어오면 400 에러를 뱉어준다 )


    요청이 들어오고 응답을 Interceptor하여 변형된 응답값을 리턴해주고 싶을땐 어떻게 해야할까?

    아이디 값을 하나 받아 하나의 데이터를 리턴해주는 GET

    컨트롤러

    @Get(':id')
      getReportById(@Param('type') pType: ReportType, @Param('id') pId: string) {
        return this.appService.getReportById(pType, pId);
      }

    서비스

    getReportById(pType: ReportType, pId: string): ReportResponseDto {
        const result = data.reports
          .filter(({ type }) => type === pType)
          .find(({ id }) => id === pId);
    
        return result;
      }

    응답값

    만약 위 처럼 서버에 저장되어있는 데이터 형태 그대로 말고 좀 다르게 변형된 형태로 return 해주고 싶으면 어떻게 해야할까?

    예를들어 updated_at이라는 데이터는 응답이 안내려가고 created_at이라는 키는 camel case로 createdAt으로 내려주고 싶다면 말이다.

    이것또한 Dto를 사용하여 만들어 줄 수 있다.

    Dto Class 만들기

    export class ReportResponseDto {
      id: string;
      source: string;
      amount: number;
    
      @Expose({ name: 'createdAt' })
      created_at: Date;
    
      @Exclude()
      updated_at: Date;
    
      type: ReportType;
    
      constructor(partial: Partial<ReportResponseDto>) {
        Object.assign(this, partial);
      }
    }
  • Exclue 데코레이터는 해당 키값을 제외 시킨다
  • Expose는 해당 키 대신에 설정한 name 프로퍼티의 값을 리턴해준다
  • 생성자를 설정한 이유는 기존 데이터를 받아 새롭게 변형된 형태의 객체를 리턴해주기 위함이다.
  • 서비스 로직

    getReportById(pType: ReportType, pId: string): ReportResponseDto {
        const result = data.reports
          .filter(({ type }) => type === pType)
          .find(({ id }) => id === pId);
    
        return new ReportResponseDto(result);
      }
  • ReportResponseDto를 리턴 타입으로 설정
  • new ReportResponseDto(result) 처럼 새롭게 만든 dto의 인스턴스 객체를 뱉어준다 ( 그러면 dto class에 정의한 객체가 나온다 )
  • Trasnform과 Intercept를 위한 환경 설정

    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({
          transform: true,
          transformOptions: {
            enableImplicitConversion: true,
          },
        }),
      );
      await app.listen(3000);
    }
    bootstrap();
  • transform을 true로 설정하여 변형 작업을 하겠다는 설정
  • transformOptions를 설정하여 절대적 변형을 사용하겠다는 설정
  • app.module.ts

    import { ClassSerializerInterceptor, Module } from '@nestjs/common';
    import { APP_INTERCEPTOR } from '@nestjs/core';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    
    @Module({
      imports: [],
      controllers: [AppController],
      providers: [
        AppService,
        {
          provide: APP_INTERCEPTOR,
          useClass: ClassSerializerInterceptor,
        },
      ],
    })
    export class AppModule {}
  • providers 내 provide: APP_INTERCEPTOR 를 사용하여 서비스 단에서 interceptor 를 실행하겠다는 설정
  • useClass 옵션을 사용하여 지정한 클래스에 정의된 dto를 사용하겠다는 설정
  • 이상 Dto의 개념과 dto를 사용하여 validation과 transform 작업을 하는 방법을 알아보았다.