-
NestJS_온라인 공연 예매 서비스 프로젝트 3편_유저기능 구현(회원가입 및 로그인[上])TIL (Today I Learned) 2023. 12. 26. 19:07
# Nest JS 프로젝트 유저 기능 구현을 시작에 앞서
지난 글에서는 엄격한 DTO 다루기와 ValidationPipe의 활용, TypeORM을 사용한 데이터베이스 연결, 그리고 Auth 모듈을 생성해 인증 및 인가 기능을 구현하는 내용을 다뤘습니다. 기본세팅과 필수기능 구현에 대해 자세한 내용을 확인하고 싶다면 아래 링크를 참고해주세요.
# Nest JS_user 및 admin기능 회원가입 및 로그인 기능 구현_기초 작업
사전준비를 위해 src 디렉토리로 이동하여 터미널에 아래의 명령어를 실행합니다. resource로 생성하면 CRUD를 같이 생성해줘서 편하지만 변경해야할 부분이 많기 때문에 따로따로 생성을 진행했습니다.(상황에 맞춰서 진행하면 됩니다.)
***user module, service, controller 생성 명령어 ***
nest g mo user nest g s user nest g co user
***user와 admin 분리를 위한 enum 설정***
보통은 다음 작업으로, user entity를 바로 작업을 해야합니다. 하지만 entity 생성을 하기 앞서서 user와 admin을 명확히 구분지어야 하기 때문에 userRole을 통해 역할을 표현하는 enum 지정해야합니다. src/user 디렉토리 내에 types 디렉토리를 추가로 생성하고, 그 안에 userRole.type.ts 파일을 만들어서 열거형(enum)을 정의해야 합니다. 이 Enum은 나중에 User와 Admin 엔터티에서 사용되며, User entity를 생성하는데 앞서 역할을 명시적으로 구분할 수 있게 됩니다.
// 사용자 역할을 정의하는 Enum입니다. export enum Role { User, // 일반 사용자 역할 Admin, // 어드민 역할 }
## enum이란?
NestJS에서의 Enum(열거형)은 TypeScript에서 지원하는 열거형(enum)과 관련이 있습니다. Enum은 명명된 상수 집합을 정의하는 TypeScript의 데이터 형식 중 하나로, 여러 개의 연관된 상수 값을 그룹화할 수 있습니다.
NestJS에서 Enum을 사용하는 이유는 주로 코드의 가독성을 높이고 특정 값의 명시성을 확보하기 위함입니다. 특히, 데이터베이스나 다른 부분에서 사용되는 상수 값들을 정의하거나, 특정한 유형의 값이 제한된 상황에서 사용될 때 Enum을 활용하면 코드를 더욱 명확하게 만들 수 있습니다.
***user entity 생성***
유저 엔터티(User Entity)의 역할은 사용자 정보를 데이터베이스에 저장하고 관리하는데 있습니다. 이제 User Entity를 생성하기 위해 다음 단계로 진행합니다. src/user 디렉토리 내에 entities 디렉토리를 새로 만들고, 그 안에 user.entity.ts 파일을 생성하여 코드를 작성합니다. 이를 통해 User entity의 구현을 시작할 수 있습니다.
import { Column, Entity, Index, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, OneToMany, JoinColumn, } from 'typeorm'; import { Role } from '../types/userRole.type'; import { Reservation } from './Reservation.entity'; @Index('email', ['email'], { unique: true }) @Entity({ name: 'users' }) export class User { @PrimaryGeneratedColumn('uuid', { name: 'id' }) id: number; @Column({ type: 'varchar', unique: true, nullable: false, name: 'email' }) email: string; @Column({ type: 'varchar', select: false, nullable: false, name: 'password' }) password: string; @Column({ type: 'varchar', nullable: false, name: 'name' }) name: string; @Column({ type: 'varchar', nullable: false, name: 'nickname' }) nickname: string; @Column({ type: 'varchar', nullable: false, name: 'gender' }) gender: string; @Column({ type: 'tinyint', nullable: false, name: 'age' }) age: number; @Column({ type: 'varchar', unique: true, nullable: false, name: 'phone' }) phone: string; @Column({ type: 'varchar', nullable: false, name: 'grade' }) grade: string; @Column({ type: 'varchar', nullable: false, name: 'permission' }) permission: string; @Column({ type: 'enum', enum: Role, default: Role.User, name: 'Role' }) role: Role; @CreateDateColumn({ name: 'createAt', comment: '생성일시' }) createdAt: Date; @UpdateDateColumn({ name: 'updateAt', comment: '수정일시' }) updateAt: Date; @DeleteDateColumn({ name: 'deleteAt', comment: '삭제일시' }) deletedAt?: Date | null; @OneToMany(() => Reservation, (reservation) => reservation.user) @JoinColumn() reservations: Reservation[]; }
role이라는 칼럼은 Role타입의 데이터를 가지게 됩니다. 이 칼럼의 기본값은 User이지만, 필요에 따라 데이터 조작을 통해Admin으로 승격시킬 수 있습니다. 다음은 admin 전용 필드 코드는 다대다 관계를 설정하는 부분입니다.
**ManyToMany 관계 설정
@ManyToMany(() => User, (user) => user.admins) // 다대다 관계 설정 @JoinTable({ name: 'admin_users', // Junction Table의 이름 joinColumn: { name: 'user_id', referencedColumnName: 'id' }, // 현재 엔터티의 외래 키 설정 inverseJoinColumn: { name: 'admin_id', referencedColumnName: 'id' }, // 연결된 엔터티의 외래 키 설정 }) admins: User[]; // 다대다 관계를 표현하기 위한 필드
1) @ManyToMany(() => User, (user) => user.admins)는 현재 엔터티(User)와 다른 엔터티(User) 간의 다대다 관계를 설정합니다.
2) admins: User[]는 실제 다대다 관계를 표현하는 필드입니다. 이 필드를 통해 하나의 어드민이 여러 유저를 가질 수 있고, 한 유저도 여러 어드민에 속할 수 있습니다.
3) nTable 데코레이터를 사용하여 Junction Table을 설정합니다. name: 'admin_users'은 Junction Table의 이름을 지정합니다. joinColumn은 현재 엔터티의 외래 키 설정을 의미합니다. 현재 엔터티는 User이며, user_id가 외래 키로 설정되어 현재 엔터티의 id와 연결됩니다. inverseJoinColumn은 연결된 엔터티의 외래 키 설정을 의미합니다. 연결된 엔터티도 User이며, admin_id가 외래 키로 설정되어 연결된 엔터티의 id와 연결됩니다.
위에 전체 코드를 표로 나타내면 이런 형태로 나타낼 수 있습니다.
## user entity란?
user entity는 소프트웨어 개발에서 엔터티(Entity) 개념 중 하나로, 사용자 정보를 나타내는 데이터 모델이나 객체를 말합니다. Entity는 어플리케이션의 데이터를 기술하며, 데이터베이스에서는 테이블로 매핑됩니다.
user entity는 주로 사용자와 관련된 정보를 담고 있는 객체 또는 데이터 모델을 지칭합니다. 이는 사용자의 이메일, 비밀번호, 역할(Role) 등을 포함할 수 있습니다. 프로젝트나 시스템에 따라 필요한 사용자 정보를 나타내기 위해 이 엔터티를 정의하고 사용합니다.
예를 들어, Nest.js에서 사용하는 TypeORM을 사용하는 경우, user entity는 데이터베이스의 users 테이블에 매핑되며, 해당 테이블은 사용자 정보를 저장합니다. 이를 위해 user entity 클래스를 작성하고, 이 클래스의 인스턴스를 생성하여 데이터베이스와 상호작용합니다. 간단한 user entity 클래스의 예시는 다음과 같습니다.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() password: string; @Column() role: string; }
이 코드에서 User 클래스는 TypeORM의 엔터티로 선언되었고, users 테이블과 매핑됩니다. 테이블의 각 열(Column)은 사용자의 id, email, password, role을 나타냅니다.
## uentity 습관 길들이기
sevice와 controller 코드를 작성하기 이전에 entity를 먼저 생성하는 것이 좋은 습관입니다. 이를 통해 자신이 어떤 기능을 구현해야하는지에 대한 청사진을 명확히 그릴 수 있습니다.
## Admin으로 변경하는 3가지 방법
1. TypeORM을 사용한 업데이트 예시
// TypeORM을 사용하여 User 엔터티의 role을 업데이트하는 서비스 import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './entities/user.entity'; import { Role } from './types/userRole.type'; @Injectable() export class UserService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} // userId를 받아 해당하는 사용자의 role을 Admin으로 업데이트 async updateUserToAdmin(userId: number): Promise<User> { // TypeORM QueryBuilder를 사용하여 엔터티 업데이트 쿼리 작성 await this.userRepository .createQueryBuilder() .update(User) .set({ role: Role.Admin }) .where('id = :id', { id: userId }) .execute(); // 업데이트된 사용자 반환 return this.userRepository.findOne(userId); } }
2. 직접 SQL 쿼리를 사용하는 방법 예시
// 직접 SQL 쿼리를 사용하여 User 엔터티의 role을 업데이트하는 서비스 import { Injectable } from '@nestjs/common'; import { getConnection } from 'typeorm'; import { Role } from './types/userRole.type'; @Injectable() export class UserService { // userId를 받아 직접 SQL 쿼리를 실행하여 role을 Admin으로 업데이트 async updateUserToAdmin(userId: number): Promise<void> { // TypeORM을 사용하지 않고 직접 SQL 쿼리 실행 const connection = getConnection(); await connection.query( 'UPDATE users SET role = $1 WHERE id = $2', [Role.Admin, userId], ); } }
3. Admin으로 업그레이드하는 메서드 작성 예시
// 사용자를 어드민으로 업그레이드하는 서비스 import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './entities/user.entity'; import { Role } from './types/userRole.type'; @Injectable() export class UserService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} // userId를 받아 기존 사용자를 삭제하고, 새로운 어드민 엔터티를 생성하여 저장 async promoteUserToAdmin(userId: number): Promise<User> { // userId로 기존 사용자 찾기 const user = await this.userRepository.findOne(userId); // 사용자가 존재하면 어드민 엔터티 생성 if (user) { const admin = new User(); admin.email = user.email; admin.password = user.password; admin.role = Role.Admin; // 새로운 어드민 엔터티 저장, 기존 사용자 엔터티 삭제 await this.userRepository.save(admin); await this.userRepository.remove(user); // 변경된 어드민 엔터티 반환 return admin; } // 사용자가 존재하지 않으면 예외 발생 throw new Error('User not found'); } }
***user.controller에서 사용할 DTO 생성하기***
다음으로 user.controller에서 사용할 DTO를 생성하기 위해 src/user 디렉토리로 이동한 후 dto 디렉토리를 생성하여 login.dto.ts 파일에 코드를 작성합니다.
import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; // 로그인 요청에 사용되는 DTO 클래스 정의 export class LoginDto { // IsEmail 데코레이터를 사용하여 email이 이메일 형식인지 검사 // IsNotEmpty 데코레이터를 사용하여 email이 비어있지 않은지 검사하고, 비어 있을 경우 지정된 에러 메시지를 반환 @IsEmail() @IsNotEmpty({ message: '이메일을 입력해주세요.' }) email: string; // IsString 데코레이터를 사용하여 password가 문자열인지 검사 // IsNotEmpty 데코레이터를 사용하여 password가 비어있지 않은지 검사하고, 비어 있을 경우 지정된 에러 메시지를 반환 @IsString() @IsNotEmpty({ message: '비밀번호를 입력해주세요.' }) password: string; }
## DTO란?(저번보다 조금 더 딥하게)
DTO는 데이터 전송 객체(Data Transfer Object)의 약자로, 주로 서버와 클라이언트 간에 데이터를 주고받을 때 사용되는 객체입니다. DTO는 어플리케이션의 여러 부분 간에 데이터 교환을 간소화하고, 데이터의 일관성을 유지하며, 데이터를 캡슐화하는 데 사용됩니다.
## DTO 사용 방법
1. 클래스로 정의
DTO는 클래스로 정의되며, 해당 클래스는 주로 속성(프로퍼티)들을 갖습니다. 각 속성은 전송하거나 받을 데이터의 특정 필드를 나타냅니다.
export class UserDto { id: number; username: string; email: string; }
2. class-validator 활용
class-validator와 같은 라이브러리를 사용하여 데이터의 유효성을 검사합니다. 이를 통해 데이터가 기대한 형식과 규칙을 따르는지 확인할 수 있습니다.
import { IsString, IsEmail } from 'class-validator'; export class UserDto { @IsString() username: string; @IsEmail() email: string; }
3. 서비스나 컨트롤러에서 활용
DTO는 주로 서비스나 컨트롤러에서 데이터 전송 및 유효성 검사에 사용됩니다. 클라이언트로부터 받은 데이터를 DTO로 매핑하고, 필요한 경우 서비스나 컨트롤러 내에서 비즈니스 로직에 활용됩니다.
import { Injectable } from '@nestjs/common'; import { UserDto } from './dto/user.dto'; @Injectable() export class UserService { createUser(userDto: UserDto): string { // 유효성 검사 및 비즈니스 로직 처리 // ... return 'User created successfully'; } }
DTO는 어플리케이션의 레이어 간의 상호 작용을 개선하고, 데이터 전송 및 유효성 검사를 효율적으로 관리할 수 있도록 도와주는 중요한 개념입니다
## DTO 사용에 관한 고려사항
1. 불변성(Immutable)을 유지하는 DTO
DTO를 불변으로 유지하면 예측 가능한 동작과 부작용을 방지할 수 있습니다.
2. DTO와 비즈니스 로직의 분리
DTO는 데이터 전송에, 비즈니스 로직은 서비스 레이어에 담당하도록 분리합니다.
3. DTO의 유효성 검사 커스터마이징
class-validator를 사용하여 DTO의 유효성을 검사하고, 필요에 따라 커스텀 규칙을 정의하거나 에러 메시지를 커스터마이징합니다.
4. DTO와 OpenAPI(Swagger) 문서 생성
명확한 DTO 정의는 프레임워크가 자동으로 생성하는 OpenAPI(Swagger) 문서를 개선하고 개발자들이 API에 쉽게 접근할 수 있도록 합니다.
5. DTO의 네스팅 (중첩)
DTO 내에서 다른 DTO를 중첩하여 복잡한 데이터 구조를 간결하게 표현하는 방법을 이해합니다.
6. DTO의 선택적 필드 (Partial DTO)
필요에 따라 DTO에서 일부 필드만 전송하고 싶을 때, 선택적인 필드를 허용하는 방법을 고려합니다.
7. DTO와 프론트엔드 통합
클라이언트와 서버 간의 DTO 일치를 유지하여 프론트엔드와 백엔드 간의 통합을 용이하게 합니다.
***커스텀데코레이터 userInfo 생성하기***
다음 작업으로 UserInfo라는 커스텀 데코레이터를 생성합니다. 이 데코레이터는 로그인이 필요한 API에서 사용자 정보를 추출하는 데에 활용됩니다. src 디렉토리로 이동 후 utils 디렉토리와 userInfo.decorator.ts 파일을 만들어 코드를 작성합니다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; // UserInfo 커스텀 데코레이터 생성 export const UserInfo = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { // ExecutionContext를 통해 현재 실행 컨텍스트를 가져옴 const request = ctx.switchToHttp().getRequest(); // 만약 request에 user 정보가 있다면 반환하고, 그렇지 않으면 null 반환 return request.user ? request.user : null; }, );
## 커스텀데코레이터란?
커스텀 데코레이터(Custom Decorator)는 TypeScript와 JavaScript에서 클래스, 메서드, 속성 등에 사용자가 직접 정의한 데코레이터입니다. Nest.js와 같은 프레임워크에서 많이 사용되며, 주로 코드의 재사용성을 높이고 가독성을 개선하기 위해 활용됩니다.
커스텀 데코레이터는 주로 createParamDecorator, createParamDecoratorFactory 등과 같은 함수를 사용하여 생성됩니다. 이 함수들은 데코레이터의 동작을 정의하고, 필요한 매개변수를 받아 사용자가 원하는 기능을 수행할 수 있도록 합니다.
커스텀 데코레이터의 사용 예시로는 주로 로깅, 권한 검사, 데이터 추출 등과 같은 특정 동작을 수행하는 로직을 추상화하고 재사용 가능한 형태로 만들 때 활용됩니다.
## 데코레이터란?
데코레이터는 @ 기호로 시작하며, 클래스, 메서드, 속성 등에 부가적인 메타데이터를 추가하거나 특정 동작을 수행하는 역할을 합니다. Nest.js에서는 커스텀 데코레이터를 만들어서 특정한 로직을 추상화하고 재사용 가능한 코드 조각으로 만들기 위해 사용됩니다.
## 커스텀 데코레이터 사용방법
1. 커스텀 데코레이터 생성
createParamDecorator, createParamDecoratorFactory 등을 활용하여 커스텀 데코레이터를 생성합니다.
// userInfo.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; // userInfo 커스텀 데코레이터 생성 export const userInfo = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user ? request.user : null; }, );
2. 커스텀 데코레이터 적용
클래스, 메서드, 속성 등에 @userInfo()와 같은 형태로 커스텀 데코레이터를 적용합니다.
// 사용 예시: 프로필 컨트롤러 import { Controller, Get, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from './jwt-auth.guard'; import { userInfo } from './path-to-userInfo.decorator'; @Controller('profile') export class ProfileController { @UseGuards(JwtAuthGuard) @Get() getProfile(@userInfo() user: any) { // @userInfo 데코레이터를 통해 로그인된 사용자 정보를 사용할 수 있음 return `Hello, ${user.username}!`; } }
3. 커스텀 데코레이터 사용
커스텀 데코레이터가 적용된 부분에서 특정 동작을 수행하거나 메타데이터를 활용합니다.
// 사용 예시: 프로필 컨트롤러 메서드 @UseGuards(JwtAuthGuard) @Get() getProfile(@userInfo() user: any) { // @userInfo 데코레이터로부터 얻은 정보 활용 return `Hello, ${user.username}!`; }
## 데코레이터 사용 시 주의사항
1. 순서에 주의
러 데코레이터가 하나의 항목에 적용될 경우, 데코레이터의 순서에 주의해야 합니다. 데코레이터는 위에서 아래로 적용되며, 순서에 따라 동작이 달라질 수 있습니다.
// 순서에 주의: 두 데코레이터가 적용된 경우 @FirstDecorator() @SecondDecorator() class ExampleClass {}
2. 함수 시그니처 이해
데코레이터 함수의 시그니처를 이해하고 활용해야 합니다. 데코레이터 함수는 특정한 시그니처를 갖고 있으며, 이를 통해 필요한 동작을 수행할 수 있습니다.
// 데코레이터 함수 시그니처 예시 function CustomDecorator(data: unknown, context: ExecutionContext): any { // 동작 정의 // ... }
3. 재사용 가능성 고려
커스텀 데코레이터를 설계할 때, 가능한한 재사용 가능한 형태로 만들어야 합니다. 특정 로직을 추상화하여 여러 곳에서 활용할 수 있도록 구성하는 것이 좋습니다.
// 재사용 가능한 데코레이터 예시 export const LogExecutionTime = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const startTime = Date.now(); const result = ctx.switchToHttp().getRequest(); console.log(`Execution Time: ${Date.now() - startTime}ms`); return result; }, );
'TIL (Today I Learned)' 카테고리의 다른 글
NestJS_온라인 공연 예매 서비스 프로젝트 5편_공연 기능 구현(수정중) (1) 2023.12.28 NestJS_온라인 공연 예매 서비스 프로젝트 4편_유저기능 구현(회원가입 및 로그인[下]) (0) 2023.12.27 NestJS_온라인 공연 예매 서비스 프로젝트 2편_프로젝트 필수 기능 구현 (0) 2023.12.24 NestJS_온라인 공연 예매 서비스 프로젝트 1편_기본 세팅 (1) 2023.12.23 #TIL(error)_NestJS 오류 : Delete`CR` (0) 2023.12.22