사용자의 정보를 받아 데이터베이스에 안전하게 저장하는 회원 가입 기능을 구현해보자.
개발 환경 : NestJS, Prisma, Swagger
CreateUserDTO와 UserDTO가 있다.CreateUserDTO는 비밀번호 같은 민감한 정보를 포함한다.UserDTO는 민감한 정보를 포함하지 않고, 클라이언트에게 노출되어도 되는 정보를 포함한다.user/dtos/create-user.dto.ts
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, MinLength, Validate } from 'class-validator';
export class CreateUserDTO {
@IsNotEmpty({ message: '아이디를 입력해 주세요.' })
@ApiProperty({ description: '아이디', example: 'username' })
username: string;
@IsNotEmpty({ message: '연락처를 입력해 주세요.' })
@ApiPropertyOptional({ description: '연락처', example: '010-1234-5678' })
contact: string;
@IsNotEmpty({ message: '비밀번호를 입력해 주세요.' })
@MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' })
@ApiProperty({ description: '비밀번호', example: 'password' })
password: string;
@IsNotEmpty({ message: '비밀번호를 다시 한 번 입력해 주세요.' })
@ApiProperty({ description: '비밀번호 확인', example: 'password' })
passwordConfirm: string;
}
class-validator 라이브러리의 ValidatorConstraint와 ValidatorConstraintInterface를 사용하여 커스텀 유효성 검사기를 만들 수 있다.validate 메서드에서 args.object를 CreateUserDTO 타입으로 캐스팅하여 password 필드와 passwordConfirm 필드의 값을 비교한다.true를 반환하고, 그렇지 않으면 false를 반환한다.validators/password-match.validator.ts
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
import { CreateUserDTO } from '../dtos/create-user.dto';
/**
* 비밀번호와 비밀번호 확인 필드가 일치하는지 확인하는 Validator
*/
@ValidatorConstraint({ name: 'PasswordMatch', async: false })
export class PasswordMatch implements ValidatorConstraintInterface {
validate(passwordConfirm: string, args: ValidationArguments) {
const object = args.object as CreateUserDTO;
return object.password === passwordConfirm;
}
defaultMessage(_args: ValidationArguments) {
return '비밀번호가 일치하지 않습니다.';
}
}
CreateUserDTO 클래스에서 PasswordMatch 유효성 검사기를 사용하여 비밀번호와 비밀번호 확인 필드의 일치를 검증할 수 있다.import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, MinLength, Validate } from 'class-validator';
import { PasswordMatch } from '../validators/password-match.validator';
export class CreateUserDTO {
@IsNotEmpty({ message: '아이디를 입력해 주세요.' })
@ApiProperty({ description: '아이디', example: 'username' })
username: string;
@IsNotEmpty({ message: '연락처를 입력해 주세요.' })
@ApiPropertyOptional({ description: '연락처', example: '010-1234-5678' })
contact: string;
@IsNotEmpty({ message: '비밀번호를 입력해 주세요.' })
@MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' })
@ApiProperty({ description: '비밀번호', example: 'password' })
password: string;
@IsNotEmpty({ message: '비밀번호를 다시 한 번 입력해 주세요.' })
@Validate(PasswordMatch, { message: '비밀번호가 일치하지 않습니다.' }) // 추가
@ApiProperty({ description: '비밀번호 확인', example: 'password' })
passwordConfirm: string;
}
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();
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import { randomUUID } from 'crypto';
@Exclude()
export class UserDTO {
@Expose()
@ApiProperty({ description: 'ID', example: randomUUID() })
id: string;
@Expose()
@ApiProperty({ description: '아이디', example: 'username' })
username: string;
@Expose()
@ApiPropertyOptional({ description: '연락처', example: '010-1234-5678' })
contact: string;
@Expose()
@ApiProperty({ description: '등록일시', type: 'Date' })
createdAt: Date;
@Expose()
@ApiProperty({ description: '수정일시', type: 'Date' })
updatedAt: Date;
}
bcrypt 라이브러리를 사용하여 비밀번호를 해싱해보자.
bcrypt 패키지 설치npm install bcrypt
npm install @types/bcrypt --save-dev
bcrypt 의 hashSync 메서드는 동기적으로 비밀번호를 해싱한다.const hashedPassword = hashSync(plainPassword, 10);
user/user.service.ts
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { Prisma, User } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDTO } from './dtos/create-user.dto';
import { hashSync } from 'bcrypt';
@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);
constructor(private readonly prismaService: PrismaService) {}
async create(body: CreateUserDTO): Promise<User> {
const check = await this.findByUsername(body.username);
if (check) throw new ConflictException('이미 사용 중인 아이디 입니다.');
return this.prismaService.user.create({
data: {
username: body.username,
contact: body.contact,
password: hashSync(body.password, 10),
},
});
}
findByUsername(username: string): Promise<User> {
return this.prismaService.user.findUnique({ where: { username } });
}
}
POST /user/create 엔드포인트에서 요청을 받도록 작성하였다.user/user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { ApiBody, ApiCreatedResponse, ApiOperation } from '@nestjs/swagger';
import { UserDTO } from './dtos/user.dto';
import { plainToInstance } from 'class-transformer';
import { CreateUserDTO } from './dtos/create-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@ApiOperation({
summary: '사용자 생성',
description: '새로운 사용자를 생성합니다.',
})
@ApiBody({ description: '사용자 생성 데이터', type: CreateUserDTO })
@Post('create')
@ApiCreatedResponse({
description: '성공적으로 생성됨',
type: UserDTO,
})
async create(@Body() body: CreateUserDTO) {
return plainToInstance(UserDTO, await this.userService.create(body));
}
}


DB 에도 비밀번호가 잘 암호화 되어 저장 되었다.

비밀번호와 비밀번호 확인이 일치하지 않는 경우 오류를 반환하고, DB에 저장하지 않는다.
