클라이언트로부터 퀘스트 시작 날짜와 종료 날짜를 2024-07-25
와 같은 문자열 형태로 받을 때, 이를 데이터베이스에 저장하기 위해 Date
객체로 변환해줄 필요가 있었다.
이 과정에서 class-transformer
의 @Transform
데코레이터를 사용하여 데이터 변환을 시도했지만, 문제가 발생했다.
처음에는 다음과 같이 @Transform
데코레이터를 사용하여 날짜 변환을 시도했다.
startDate
와 endDate
를 property
에 담아서 넘겨주기 때문에, value
를 추출해서 원하는 형태로 변환하도록 설정하였다.
// create-quest.request.ts
export class CreateQuestRequest {
...
@IsDateString()
@IsNotEmpty()
@Transform((property) => {
return toUTCStartOfDay(property.value);
})
startDate: Date;
@IsDateString()
@IsNotEmpty()
@Transform((property) => {
return toUTCEndOfDay(property.value);
})
endDate: Date;
}
위와 같은 방식은 다음과 같은 검증 오류를 발생시켰다.
{
"statusCode": 400,
"timestamp": "2024-07-24T14:08:12.825Z",
"path": "/quests",
"method": "POST",
"message": "Bad Request Exception",
"details": [
"startDate: startDate must be a valid ISO 8601 date string",
"endDate: endDate must be a valid ISO 8601 date string"
]
}
상세 메세지만 봤을 때는 전달받은 value
가 ISO 8601
형태가 아니기 때문에, 발생한 문제로 생각했지만, 디버깅 과정에서 class-transformer
와 class-validator
의 동작 순서가 예상과 달랐음을 발견했다.
@IsDateString()
데코레이터를 통해 value
를 검증 후, @Transform
데코레이터를 이용하여 변환될 것으로 예상했는데, 먼저 @Transform
데코레이터를 통해 Date
객체로 변환되었다.
그 후 @IsDateString()
데코레이터를 통해 검증하기 때문에, 오류가 발생한 것이다.
위 문제의 핵심은 NestJS에서 class-transformer
와 class-validator
의 동작 순서에 있다.
클라이언트로부터 HTTP 요청이 서버로 전송되고, class-transformer
를 이용해 요청의 JSON 형태인 body 값을 지정된 클래스의 인스턴스로 변환한다.
그 후, class-validator
를 이용하여 생성된 인스턴스에 대해 유효성 검사를 수행하게 된다.
따라서, class-transformer
를 이용하여 인스턴스로 변환하는 과정에서 @Transform
데코레이터가 먼저 동작하게 되는 것이다
이 문제를 해결하기 위해 커스텀 데코레이터 @TransformDateToUTC
를 만들었다.
이 데코레이터는 전달받은 option
에 따라 value
를 원하는 형태로 변환하는 데코레이터다.
export type TransformDateOptions = {
option: 'start' | 'end';
};
export function TransformDateToUTC({ option }: TransformDateOptions): PropertyDecorator {
return Transform(({ value }) => {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (typeof value !== 'string' && !regex.test(value) && !dayjs(value).isValid()) {
throw new BadRequestException(`Please provide only date like 'YYYY-MM-DD'`);
}
return option === 'start' ? toUTCStartOfDay(value) : toUTCEndOfDay(value);
});
}
ts
// create-qeust.request.ts
export class CreateQuestRequest {
...
@TransformDateToUTC({ option: 'start' })
@IsNotEmpty()
@IsDate()
startDate: Date;
@IsNotEmpty()
@TransformDateToUTC({ option: 'end' })
@IsNotEmpty()
@IsDate()
endDate: Date;
}
NestJS에서 응답/요청 객체 직렬화 (Serialization) 하기
NestJS class-validator와 transformer가 데코레이터와 다른점
[TS] class-validator의 활용과 검증 옵션