본문 바로가기

카테고리 없음

끝판왕의 프로젝트_Nest.js_공연 예매 사이트 만들기(1)_회원가입

Node.js 강의가 어느덧 슬슬 마무리가 되어가고 있다. 

최근에는 Nest.js라는 프레임워크를 새롭게 배우고 있는데 Nest의 경우 타입스크립트를 사용하기 때문에 필연적으로 타입스크립트를 배울 수 밖에 없다. 

 

이번 프로젝트는 배운 내용을 활용하여 공연 예매 사이트를 만드는 것이다. 

그 중에서 오늘은 회원가입 기능까지 구현이 되었기에 초기 세팅과 회원가입까지의 간략한 과정과 발생한 문제를 정리해보도록 하겠다.

[간단 요약]
1. 초기 세팅
- ERD 설계
- nest new juns_ticket
- nest generate resource auth

2.  회원가입 
- controller
- service
- repository

 

#1. 초기 세팅

1) ERD 설계

본격적인 코딩에 앞서 이번 프로젝트에서 사용할 DB를 설계하였다.

(물론 코딩을 하면서 중간중간 변경사항이 있을 수 있겠지만 사전에 어느정도 ERD를 설계해야 코딩하는데 문제가 없다.) 

이번 프로젝트의 ERD

우선 코딩을 하면서 일부 수정사항이 있을 수 있다는 것을 밝히고, 특이사항들만 기재하고 넘어가겠다. 

 

1. deletedAt
기존 DB에는 없었던 컬럼인데, 이는 typeORM에서 지원하는 softDelete 기능을 사용하고자 한다.
softDelete란 기본적인 Delete(HardDelete)와는 다르게 데이터가 삭제되지 않고 deletedAt에 날짜가 찍힌다.
즉, 기존 데이터를 보존하면서 사용하지 않는 데이터를 관리하고자 할 때 사용한다. 

2. status
ERD 상에서는 status라는 상태값이 2개 컬럼에 들어가있다. 
각 각의 이용하고자 하는 용도가 있는데 

-Reservation 의 경우 
status 값을 booking, canceled, finished 3개의 값을 enum으로 가진다. 
이유는 본인의 예약을 조회하였을 때 상태값별로 조회를 하고자 함에 있고, 추후 finished(공연이 끝난 경우) 상태인 경우에만 후기를 남길 수 있게 하기 위함이다.

-Seats의 경우
status 값을 Complete, Possible 2개의 값을 enum으로 가진다.
특정 좌석을 클라이언트에게 보여줄 때 상태값이 Possible인 값의 좌석만 보여주고자 한다. 
당연히 그 좌석이 예약되었을 경우 상태값이 변경하고자 한다.

3. 관계
각 테이블 간의 관계는 아래와 같다. (편의상 간략하게 표현하도록 하겠다.)

Users:Reservations = 1:N
Performances:Reservations = 1:N
Performances:Seats = 1:N

아직 테이블 관계를 설정하는 부분이 굉장히 헷갈리지만 .. 꾸준히 하다보면 이해가 되리라 ㅠ

 

 

2) nest new !

본격적으로 nest 프레임워크를 이용한 프로젝트를 생성하고자 한다.

먼저 나는 맥을 사용하고 있으므로 터미널을 이용했다.

 

1. 터미널에서 본인이 프로젝트를 생성하고자 하는 디렉토리로 이동한다. 

cd <원하는 디렉토리명>

 

2. nest 명령어를 이용하여 프로젝트를 생성한다.

(물론 nest와 typescript가 컴퓨터에 설치되어 있어야 한다.)

nest new <프로젝트명>

// 예시
nest new juns_ticket

위 명령어를 입력하면 패키지 매니저로 어떤 것을 이용할 것인지 묻는데 강의시간에서는 npm이 nest와 가장 호환성이 좋다고 하며, yarn의 경우 일부 오류가 나는 경우도 있다고 하여 npm을 선택해주면 무난할듯 하다. 

 

설치하고 VS코드를 실행시키면 여러가지 파일들이 함께 설치된 것을 볼 수 있다. 

 

3. typeORM을 포함하여 작업에 필요한 패키지를 설치한다.

npm i @nestjs/config @nestjs/jwt @nestjs/passport passport passport-jwt @types/passport-jwt typeorm @nestjs/typeorm mysql2 multer bcrypt @types/bcrypt class-validator class-transformer multer @types/multer papaparse @types/papaparse joi typeorm-naming-strategies lodash @types/lodash

우선 강의시간에 진행한 프로젝트에서 사용한 패키지 목록이다. 

 

passport의 경우 인증을 쉽게 도와주는 패키지이고, bcrypt는 비밀번호를 해쉬하는 패키지이다. 

이처럼 본인이 필요한 패키지를 먼저 설치하고 코딩 중 더 필요한 부분이 있다면 쉽게쉽게 설치하면 된다. 

 

 

3) nest g res auth

nest라는 명령어를 터미널에 입력해보면 내가 사용할 수 있는 다양한 명령어들을 확인할 수 있다. 

nest 명령어들

nest g res auth

위 명령어는 nest generate resource auth 의 줄임 표현이다. 

 

nest의 경우 3-layered-architecture를 프레임워크에서 나눠준다. (내가 파일을 만들고 나눌 필요가 없다.) 

 

즉 user와 관련한 controller, service 파일을 만들어주는 명령어이고,

위 명령어를 입력하면 REST API를 사용할 것인지, 기본적인 CRUD가 필요한지 묻는다. (아주 친절한 녀석) 

Nest.js가 만들어준 컨트롤러 파일

위 사진을 보면 기본적인 CRUD를 작성할 틀을 만들어두었다. 

나는 이 틀 안에서 놀기만 하면됨 !

 

#2. 회원가입

1) controller

나는 auth라는 리소스를 따로 관리하고자 했다. 

user와는 성격이 조금 다르다고 생각이 들었고, 회원가입과 로그인을 별도로 관리하는게 기능별로 유지보수가 편하다고 느꼈기 때문이다. 

물론 서비스 파일에서 auth라는 레포지토리를 새로 생성하는 것이 아니라 user라는 레포지토리를 그대로 사용하기는 한다. 

/* auth.controller.ts */

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignUpDto } from './dto/sign-up.dto';
import { SignInDto } from './dto/sign-in.dto';

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('sign-up')
  async signUp(@Body() userInfo: SignUpDto) {
    const user = await this.authService.signUp(userInfo);
    return {
      success: 'true',
      message: '회원가입에 성공했습니다.',
      data: user,
    };
  }

  @Post('/sign-in')
  signIn(@Body() user: SignInDto) {
    return this.authService.signIn(user);
  }
}

 

SignUpDto파일은 아래와 같다.

/* sign-up.dto.ts */

import {
  IsEmail,
  IsEnum,
  IsNotEmpty,
  IsString,
  MinLength,
} from 'class-validator';

enum Role {
  'admin',
  'customer',
}

export class SignUpDto {
  @IsString()
  @IsNotEmpty({ message: '이름을 입력해주세요.' })
  readonly name: string;

  @IsString()
  @IsNotEmpty({ message: '비밀번호를 입력해주세요.' })
  @MinLength(6, { message: '비밀번호는 최소 6자 이상을 입력해야 합니다.' })
  readonly password: string;

  @IsString()
  readonly confirmPassword: string;

  @IsEmail({}, { message: '유효하지 않은 이메일 입니다.' })
  @IsNotEmpty({ message: '이메일을 입력해주세요.' })
  readonly email: string;

  @IsEnum(Role, { message: '역할은 admin과 customer에서 골라주세요.' })
  readonly role: Role;
}

해당 파일에서는 class-validator 라이브러리를 활용하여 Body에서 입력받는 데이터들에 대한 유효성 검사를 1차로 실행한다.

 

아 ! 

user.entity.ts 파일은 아래와 같다.

엔티티 파일은 auth리소스 안에 있는 것이 아니라 user라는 리소스를 생성하여 그 안에 엔티티 파일을 이용한다.

/* user.entity.ts */

import { IsDate, IsEmail, IsEnum, IsNumber, IsString } from 'class-validator';
import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

export enum Role {
  'admin',
  'customer',
}
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @IsString()
  @Column('varchar', { length: 50, nullable: false })
  name: string;

  @IsString()
  @Column('varchar', { length: 500, nullable: false, select: false })
  password: string;

  @IsEmail()
  @Column('varchar', { length: 255, nullable: false, unique: true })
  email: string;

  @IsNumber()
  @Column('int', { nullable: false, default: 1000000 })
  point: number;

  @IsEnum(Role)
  @Column('varchar', { nullable: false })
  role: Role;

  @IsDate()
  @CreateDateColumn()
  createdAt: Date;

  @IsDate()
  @UpdateDateColumn({ default: null })
  updatedAt: Date;

  @IsDate()
  @DeleteDateColumn({ default: null })
  deletedAt: Date;
}

앞서 설계한 ERD를 토대로 엔티티 파일을 구성했다. 

@DeleteDateColumn 이라는 어노테이션을 통해 SoftDelete를 사용할 수 있는 조건을 만들어주었다. 

추후 삭제되지 않은 데이터를 가지고 오고 싶을 때에는 해당 컬럼이 null이 아닌 값들을 가져와야 한다. 

 

2) Service

/* auth.service.ts */

import { BadRequestException, Injectable } from '@nestjs/common';
import { SignUpDto } from './dto/sign-up.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import { SignInDto } from './dto/sign-in.dto';
import bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    // 레포지토리 생성
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}

  async signUp(userData: SignUpDto) {
    const { name, password, confirmPassword, email, role } = userData;

    // 입력받은 password와 confirmPassword 가 다를 경우 에러 반환
    if (password !== confirmPassword) {
      throw new BadRequestException('비밀번호를 확인해주세요');
    }

    if (await this.findUserByEmail(email)) {
      throw new BadRequestException('이미 존재하는 이메일 입니다.');
    }

    // 입력받은 Password를 해쉬화
    const hashedPassword: string = await bcrypt.hash(password, 10);

    const newUser: User = await this.userRepository.save({
      name,
      password: hashedPassword,
      email,
      role,
    });

    return {
      id: newUser.id,
      name: newUser.name,
      email: newUser.email,
      point: newUser.point,
      role: newUser.role,
    };
  }

  signIn(user: SignInDto) {
    return `Sign-In user`;
  }

  async findUserByEmail(email): Promise<boolean> {
    const user = await this.userRepository.findOne({
      where: { email },
    });

    if (!user) {
      return false;
    }

    return true;
  }
}

DTO파일에서 class-validation으로 해결하지 못한 유효성 검사 일부를 실행하였다. 

회원가입 시 비밀번호는 bcrypt를 통해 해시하고, 응답 시 가입된 회원정보 중 password를 제외하고 보여주면 좋을 정보들을 응답한다.

(사실 응답이 필요 없을 듯하여 지워도 될것 같다.)

 

이렇게 구성하여 회원가입까지 구현이 끝났다. 

express가 익숙하기 때문에 아직은 어색함이 많지만 여러가지 유저의 편의를 위한 기능들이 많이 있는 nestjs같다. 

 

다음에는 로그인 기능을 구현해보도록 하겠다.