지금 하고 있는 프로젝트를 express에서 Nest.js로 리팩토링하면서 있었던 일을 정리해보겠습니다.
공식문서를 기반으로 DTO를 재설계하면서 공부한 내용을 정리해봤습니다. (Nest.js는 9.2.0버전입니다)
Mapped types란?
CRUD와 같은 기능을 만들 때 기본 Entity Type에 변형해서 생성하는 것이 유용한 경우가 많습니다. Nest는 이 작업을 보다 편리하게 하기 위해 타입 변환을 수행하는 여러 유틸리티 기능(이하 4개의 함수)을 제공합니다.
- PartialType, PickType, OmitType, IntersectionType
PartialType
input validation types(이하 DTOs라 칭함)을 만들 때, 우리는 종종 create와 update variation들에서 동일한 타입을 쓰는 것이 유용할 때가 많습니다. 그렇지만 create variant는 모든 field를 요구하고, update variant는 부분적인 field를 요구합니다.
*variant : 가변요소
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty()
name: string;
@ApiProperty()
age: number;
}
이런식으로 PartialType 함수로 다른 DTO를 상속 받아오면서도 각 field를 optional하게 가져올 수 있습니다.
export class UpdateUserDto extends PartialType(CreateUserDto) {}
PickType
PickType 함수는 input type에서 properties 집합을 선택하여 new type(class)을 구성합니다. 즉, 부분적으로 포함 시키고 싶은 프로퍼티들만 골라서 상속받을 수 있다는 것이다.
export class UpdateUserAgeDto extends PickType(CreateUserDto, ['age'] as const) {}
OmitType
OmitType 함수는 input type에서 모든 properties를 선택한 다음, 특정 keys 집합을 제거하여 type을 구성합니다. 즉, PickType 함수와는 반대로 포함시키지 않을 프로퍼티들을 골라서 나머지를 상속받을 수 있다는 것이다.
export class UpdateUserDto extends OmitType(CreateUserDto, ['name'] as const) {}
IntersectionType
IntersectionType 함수는 2개의 types을 합쳐서 하나의 새로운 type을 구성합니다.
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty()
name: string;
@ApiProperty()
age: number;
}
export class AdditionalUserInfo {
@ApiProperty()
gender: string;
}
export class UpdateUserDto extends IntersectionType(
CreateUserDto,
AdditionalUserInfo,
) {}
Composition
위 4개의 타입은 아래와 같이 composable하게 중첩해서 사용하는 것도 가능합니다.
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['name'] as const),
) {}
출처(docs.nestjs.com) : https://docs.nestjs.com/openapi/mapped-types
Before
(Fault.1) 각각의 CRUD fuction마다 이런식으로 class를 만들어서 관리했고, 여러 DTO의 value가 중복되는 문제가 발생했습니다.
// users/dto/create-user.dto.ts
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsInt, IsString, Length, Max, MaxLength, Min, MinLength } from 'class-validator';
export class CreateUserDto {
...
@IsString()
@IsEmail()
@MaxLength(50)
readonly email?: string | null;
@IsInt()
@Min(0)
@Max(100)
readonly age?: number | null;
...
}
// users/dto/update-user.request.ts
import { ApiProperty } from '@nestjs/swagger';
export class UpdateUserDto {
@ApiProperty({
example: 'example@gmail.com',
description: '이메일',
})
readonly email?: string | null;
@ApiProperty({
example: 23,
description: '나이',
})
readonly age?: number | null;
...
}
(Fault.2) controller나 services에서 직접적으로 Entity type를 사용해서 validation이나 trasfer object로 사용했습니다. + entity 내에 있는 모든 value에 접근할 수 있는 문제도 있었습니다.
// users/users.controller.ts
@Get('/social/:social_id')
async findBySocialId(
@Param('social_id')
socialId: string,
): Promise<UserEntity> { // User DB를 관리하는 entity
...
}
After
UserEntity를 UserDto가 상속해오고, UserDto를 CreateUserDto나 UpdateUserDto가 재상속하는 방법으로 리팩토링했다.
이때 validation까지 entity에 데코레이터로 처리함으로써 DTO들은 간결하게 정리할 수 있었다.
// user.entity.ts
import { Column, Entity, OneToMany } from 'typeorm';
import { CoreEntity } from './core.entity';
import { IsBoolean, IsEmail, IsInt, IsOptional, IsString, Length, Max, MaxLength, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
@Entity('user')
export class UserEntity extends CoreEntity {
...
@ApiProperty({ example: 'hello@example.com', description: '이메일' })
@IsOptional()
@IsString()
@IsEmail()
@MaxLength(50)
@Column({ type: 'varchar', length: 50, comment: '유저 이메일', default: null })
email?: string | null;
@ApiProperty({ example: 23, description: '나이' })
@IsOptional()
@IsInt()
@Min(0)
@Max(100)
@Column({ type: 'tinyint', comment: '유저 나이', default: null })
age?: number | null;
...
}
// user.dto.ts
import { OmitType, PartialType, PickType } from '@nestjs/swagger';
import { UserEntity } from 'src/entities/user.entity';
export class UserDto extends OmitType(UserEntity, ['created_at', 'updated_at', 'deleted_at'] as const) {}
export class UpdateUserDto extends PartialType(PickType(UserDto, ['email', 'age', 'gender', 'living_exp'] as const)) {}
오류나 더 좋은 방법이 있다면, 댓글로 피드백 주시면 감사하겠습니다 :)