-
Notifications
You must be signed in to change notification settings - Fork 2
[준섭] 1121(화) 개발기록 ‐ SignUpDto에 ClassValidator 적용
$ yarn workspace server add class-validator class-transformer
- @IsNotEmpty(): 값이 비어있지 않은지 확인합니다.
- @IsString(): 문자열인지 확인합니다.
- @IsInt(): 정수인지 확인합니다.
- @IsBoolean(): 부울(Boolean) 값인지 확인합니다.
- @IsEmail(): 이메일 형식인지 확인합니다.
- @IsEnum(): 열거형(enum) 값인지 확인합니다.
- @MinLength(): 최소 길이를 설정하여 문자열 타입의 값의 길이가 해당 값보다 큰지 확인합니다.
- @MaxLength(): 최소 길이를 설정하여 문자열 타입의 값의 길이가 해당 값보다 큰지 확인합니다.
- @Min(): 최소값을 설정하여 숫자 타입의 값이 해당 값보다 큰지 확인합니다.
- @Max(): 최대값을 설정하여 숫자 타입의 값이 해당 값보다 작은지 확인합니다.
- @IsOptional(): 값이 선택적인지 확인합니다.
- @IsNumber(): 숫자인지 확인합니다.
- @IsDate(): 날짜 형식인지 확인합니다.
- @IsArray(): 배열인지 확인합니다.
- @Length(): 문자열의 길이를 확인합니다.
- @IsUUID(): UUID 형식인지 확인합니다.
- @IsNotEmptyObject(): 빈 객체가 아닌지 확인합니다.
- @Matches(): 정규식 패턴에 맞는지 확인합니다.
- @IsFQDN: 완전한 도메인 이름인지 확인합니다.
class-transformer는 객체의 속성을 변환하거나 객체 간의 변환, 객체의 일부 속성을 숨기는 역할을 할 수 있음
-
객체의 속성 변환:
- 객체의 특정 속성을 원하는 형식으로 변환합니다. 예를 들어, 날짜 형식의 문자열을 JavaScript Date 객체로 변환하거나, 반대로 Date 객체를 문자열로 변환하는 등의 작업을 할 수 있습니다.
-
객체 간의 변환:
- 하나의 객체를 다른 형태의 객체로 변환합니다. 이는 DTO 객체로의 변환 또는 서로 다른 API 응답 형식으로의 변환이 될 수 있습니다.
-
객체의 일부 속성 숨기기:
- 객체의 특정 속성을 숨기거나 선택적으로 노출합니다. 이를 통해 응답 객체에서 일부 정보를 제외하거나 가공할 수 있습니다.
import { Transform, Type } from 'class-transformer';
class User {
name: string;
@Transform(value => new Date(value)) // 문자열을 Date 객체로 변환
createdAt: Date;
@Type(() => Address) // Address 클래스로 객체 변환
address: Address;
}
// auth/dto/signup-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength, MaxLength, Matches } from 'class-validator';
export class SignUpUserDto {
@ApiProperty({
description: '로그인용 아이디',
example: 'test user',
required: true,
})
@IsNotEmpty() // not null
@IsString() // string 타입이여야 함
@MaxLength(50) // 최대 길이 50
@MinLength(4) // 최소 길이 4
@Matches(/^[a-zA-Z0-9]*$/, { // 영어와 숫자로만 이루어져야 한다
message: 'Password must contain only letters and numbers',
})
username: string;
@ApiProperty({
description: '비밀번호',
example: 'test password',
required: true,
})
@IsNotEmpty()
@IsString()
@MaxLength(100) // 최대 길이 100
@MinLength(8) // 최소 길이 8
password: string;
@ApiProperty({
description: '닉네임',
example: 'test nickname',
required: true,
})
@IsNotEmpty()
@IsString()
@MaxLength(50) // 최대 길이 50
@MinLength(2) // 최소 길이 2
@Matches(/^[a-zA-Z0-9가-힣]+$/, {
message: '닉네임은 영문자, 숫자, 한글만 사용할 수 있습니다.',
})
nickname: string;
}
// auth/auth.controller.ts
@Post()
@UsePipes(ValidationPipe) // 파이프 사용
signUp(@Body() signUpUserDto: SignUpUserDto): Promise<Partial<User>> {
return this.authService.signUp(signUpUserDto);
}
// auth/auth.service.ts
async signUp(signUpUserDto: SignUpUserDto): Promise<Partial<User>> {
const { username, nickname } = signUpUserDto;
await Promise.all([ // 미리 만들어 놓은 메서드 이용
this.isAvailableNickname(nickname),
this.isAvailableUsername(username),
]);
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(signUpUserDto.password, salt);
const newUser = this.authRepository.create({
...signUpUserDto,
password: hashedPassword,
});
const createdUser: User = await this.authRepository.save(newUser);
createdUser.password = undefined;
return createdUser;
}
위에서 사용한 isAvailableNickname, isAvailableUsername 메서드는 미리 만들어 놓은 메서드로, 각각 nickname과 username에 대하여 중복된 user가 있으면 ConflictException을 발생시킨다.
Promise.all로 구현을 하고 이미 데이터베이스에 저장된 username, nickname을 담아서 signUp 테스트를 해보니 두 에러가 랜덤으로 번갈아 가면서 나온다.
Promise.all로 묶은 두 메서드 중 더 빨리 Exception이 발생한 쪽의 메세지가 응답되는 것이다.
현재 SignUpUserDto의 프로퍼티들 위에 데코레이터들이 너무 많다고 느껴짐
너무 많다보니 가독성이 떨어진다고 느껴져서 커스텀 데코레이터로 데코레이터 수를 조금 줄이고자 함
다른 곳에서도 반복적으로 사용되는 IsNotNull, IsString은 두고 길이, 정규표현식에 대해서만 건들고자 함
// auth/decorators/signup-constraints.decorator.ts
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
// username에 대한 제약조건
@ValidatorConstraint({ name: 'isUsername', async: false })
export class IsUsernameConstraint implements ValidatorConstraintInterface {
validate(username: string) {
const isEngAndNum = /^[a-zA-Z0-9]+$/.test(username); // 영어 또는 숫자로만 이루어져야 함
const checkLength = username.length >= 4 && username.length <= 50; // 길이는 4~50
return isEngAndNum && checkLength;
}
defaultMessage() { // validate 리턴값이 false일 경우 출력되는 메세지
return '아이디는 영문자와 숫자로 이루어진 4~50자여야 합니다.';
}
}
// 위의 Validator를 데코레이터로 만들어줌
export function IsUsername(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
// Object는 데코레이터가 적용되는 클래스의 인스턴스. propertyName은 데코레이터가 적용되는 프로퍼티의 이름
registerDecorator({ // 데코레이터로 등록
target: object.constructor, // SignUpUserDto 클래스의 생성자가 타겟(생성자에 validate 로직이 들어가는 듯 하다)
propertyName: propertyName, // username 프로퍼티
options: validationOptions, // 유효성 검사 시 사용될 추가적인 옵션들
constraints: [], // 추가적인 제약 조건
validator: IsUsernameConstraint, // 위에서 구현한 validator 클래스
});
};
}
위 데코레이터는 @IsUsername()
이렇게 쓰이지만 만약 @MinLength(4)
처럼 추가적인 옵션을 더 받아오고 싶다면 validate 생성자에 args: ValidationArguments
와 같은 파라미터를 넣어주면 된다.
예를 들어 @IsUsername(option1, option2)
이런 식으로 파라미터를 넘겨준다고 하면 아래와 같이 받아올 수 있다.
@ValidatorConstraint({ name: 'isUsername', async: false })
export class IsUsernameConstraint implements ValidatorConstraintInterface {
validate(username: string, args: ValidateArguments) { // args를 받아온다.
args[0] // option1
args[1] // option2
// 생략 ..
}
defaultMessage(args: ValidationArguments) {
const [option1, option2] = args.constraints;
return `${option1}, ${option2}`;
}
}
위 과정에서 문자열이나 숫자형 등을 하드코딩한 부분이 마음에 들지 않아서 Enum을 추가했다.
// auth/enums/signup.enum.ts
export enum SignUpEnum {
MIN_USERNAME_LENGTH = 4,
MAX_USERNAME_LENGTH = 50,
MIN_PASSWORD_LENGTH = 8,
MAX_PASSWORD_LENGTH = 100,
MIN_NICKNAME_LENGTH = 2,
MAX_NICKNAME_LENGTH = 50,
USERNAME_NOTEMPTY_MESSAGE = '아이디는 필수 입력값입니다.',
PASSWORD_NOTEMPTY_MESSAGE = '비밀번호는 필수 입력값입니다.',
NICKNAME_NOTEMPTY_MESSAGE = '닉네임은 필수 입력값입니다.',
USERNAME_ISSTRING_MESSAGE = '아이디는 문자열이어야 합니다.',
PASSWORD_ISSTRING_MESSAGE = '비밀번호는 문자열이어야 합니다.',
NICKNAME_ISSTRING_MESSAGE = '닉네임은 문자열이어야 합니다.',
VIOLATE_USERNAME_MESSAGE = `아이디는 영문자와 숫자로 이루어진 ${MIN_USERNAME_LENGTH}~${MAX_USERNAME_LENGTH}자여야 합니다.`,
VIOLATE_PASSWORD_MESSAGE = `비밀번호는 ${MIN_PASSWORD_LENGTH}~${MAX_PASSWORD_LENGTH}자여야 합니다.`,
VIOLATE_NICKNAME_MESSAGE = `닉네임은 영문자, 숫자, 한글로 이루어진 ${MIN_NICKNAME_LENGTH}~${MAX_NICKNAME_LENGTH}자여야 합니다.`,
}
// signup-user.dto.ts
import ...
export class SignUpUserDto {
@ApiProperty({
description: '로그인용 아이디',
example: 'test user',
required: true,
})
@IsNotEmpty({ message: SignUpEnum.USERNAME_NOTEMPTY_MESSAGE as string })
@IsString({ message: SignUpEnum.USERNAME_ISSTRING_MESSAGE as string })
@IsUsername()
username: string;
@ApiProperty({
description: '비밀번호',
example: 'test password',
required: true,
})
@IsNotEmpty({ message: SignUpEnum.PASSWORD_NOTEMPTY_MESSAGE as string })
@IsString({ message: SignUpEnum.PASSWORD_ISSTRING_MESSAGE as string })
@IsPassword()
password: string;
@ApiProperty({
description: '닉네임',
example: 'test nickname',
required: true,
})
@IsNotEmpty({ message: SignUpEnum.NICKNAME_NOTEMPTY_MESSAGE as string })
@IsString({ message: SignUpEnum.NICKNAME_ISSTRING_MESSAGE as string })
@IsNickname()
nickname: string;
}
// signup-constraints.decorator.ts
import ...
@ValidatorConstraint({ name: 'isUsername', async: false })
export class IsUsernameConstraint implements ValidatorConstraintInterface {
validate(username: string): boolean {
const isEngAndNum = /^[a-zA-Z0-9]+$/.test(username);
const checkLength =
username.length >= SignUpEnum.MIN_USERNAME_LENGTH &&
username.length <= SignUpEnum.MAX_USERNAME_LENGTH;
return isEngAndNum && checkLength;
}
defaultMessage(): string {
return SignUpEnum.VIOLATE_USERNAME_MESSAGE;
}
}
// 생략 ..
@ValidatorConstraint({ name: 'isPassword', async: false })
export class IsPasswordConstraint implements ValidatorConstraintInterface {
validate(password: string): boolean {
return (
password.length >= SignUpEnum.MIN_PASSWORD_LENGTH &&
password.length <= SignUpEnum.MAX_PASSWORD_LENGTH
);
}
defaultMessage(): string {
return SignUpEnum.VIOLATE_PASSWORD_MESSAGE;
}
}
// 생략 ..
@ValidatorConstraint({ name: 'isNickname', async: false })
export class IsNicknameConstraint implements ValidatorConstraintInterface {
validate(nickname: string): boolean {
const isEngAndNumAndKor = /^[a-zA-Z0-9가-힣]+$/.test(nickname);
const checkLength =
nickname.length >= SignUpEnum.MIN_NICKNAME_LENGTH &&
nickname.length <= SignUpEnum.MAX_NICKNAME_LENGTH;
return isEngAndNumAndKor && checkLength;
}
defaultMessage(): string {
return SignUpEnum.VIOLATE_NICKNAME_MESSAGE;
}
}
// 생략 ..
만약 조건이나 메세지가 바뀌더라도 enum만 수정을 하면 되니 잘 바꾼 것 같다!
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)