본문 바로가기
개인과제,팀과제

Trello Service 클론코딩/회원가입

by 새싹개발자1호 2024. 7. 22.

app.module / 데이터베이스 와 서버 연결

configs 디렉토리 생성후, 클린코딩을 위해 따로 app.module 에 들어갈 내용들 관리하기 
(예: DB연결 , 서버 포트, joi를 이용한 validation 체크)

env-validation.config.ts
database.config.ts
app.module

이렇게 따로 관리해두면 app.module 을 정말 깔끔하게 관리할수 있어서 보기가 너무 좋다.

 

실제 데이터베이스에 저장될 컬럼을 정의하기위해 User 엔티티 작성

엔티티를 보면 @IsEmail 과 같은것들을 class-validator 라고 하는것들이다
그리고 deletedAt 에 ? 로 nullable 을 준 이유는 추후 softDelete 로 관리할려고 하는 이유이다
password 부분의 select: false 는 추후 나중에 return 을 반환 할때, password 를 포함하지않고
반환 하게끔 만들어준것
role 을 enum 으로 정의해준 이유는 추후 이메일인증을 도입할거고, 인증을 만료한 인원을 분류
하게끔 하기 위해 enum으로 설정하였습니다.

 

회원가입 DTO 작성해주기

위 코드를 설명하면,
SignupDto class를 PickType을 통해 User 엔티티에서 정의한 email, password, username을 가져오고
그후 추가한 confirmPassword 를 추가함으로 가입시 Body 로 전달해야할 내용을 정해주기위해
dto 를 작성하였습니다
PickType 을 swagger로 가져온 이유는 추후 swagger를 사용해 api 를 테스트 하기위함

 

회원가입 Controller 작성해주기

실제 회원가입 기능을 위한 컨트롤러 를 작성해주었습니다.
먼저 authService 에서 구현할 signUp 메서드를 사용하기 위해
constructor(생성자) 를 통해 사용할수 있게끔 정의해고 로직작성을 진행해보겠습니다.

@Body() 는 실제로 유저가 Request를 할때 Body로 데이터를 전송할수 있게끔 해주는것입니다
signUpDto 를 파라미터로 전달한 이유는, 유저가 Body 로 데이터를 전송할때 어떠한 
인덱스를 가지고 있어야할지를 정의해주기 위함입니다.

이모든걸 동기적으로 진행해야하기 때문에 async await 을 사용해 주었습니다.

 

회원가입 을 구현 해보자

import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';
import { User } from 'src/user/entities/user.entity';
import { Repository } from 'typeorm';
import { SignupDto } from './dtos/sign-up.dto';
import * as bcrypt from 'bcrypt'
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class AuthService {
    constructor (
        @InjectRepository(User)
        private readonly userRepository:Repository<User>
    ){}

    private async hashPassword(password:string){
        try{
            return bcrypt.hash(password,10);
        } catch (error){
            throw new InternalServerErrorException('비밀번호 해싱중 오류가 발생하였습니다')
        }
    }

    async signUp({email, password, confirmPassword, username}: SignupDto){
        if(password !== confirmPassword){
            throw new BadRequestException('비밀번호가 일치하지 않습니다.')
        }
        try{
            const existUser = await this.userRepository.findOneBy({email})
            if(existUser){
                throw new BadRequestException('사용중인 이메일입니다.')
            }
            const hashedPassword = await this.hashPassword(password)
            const user = this.userRepository.create({
                email,
                password: hashedPassword,
                username,
            });
            await this.userRepository.save(user)
            return{ email: user.email}
            } catch (error){
            if(error instanceof BadRequestException){
                throw error
            }
            throw new InternalServerErrorException('회원가입중 오류가 발생하였습니다')
        }
    }
}

이게 단순하게 회원가입을 위한 로직이다
한번 코드를 한줄한줄 해석해보기로하자

@Injectable()
export class AuthService {
    constructor (
        @InjectRepository(User)
        private readonly userRepository:Repository<User>
    ){}

    private async hashPassword(password:string){
        try{
            return bcrypt.hash(password,10);
        } catch (error){
            throw new InternalServerErrorException('비밀번호 해싱중 오류가 발생하였습니다')
        }
    }

먼저 실제 User엔티티 에 의해 생성된 User테이블에 접근하기위해 

 @InjectRepository(User)
        private readonly userRepository:Repository<User>

@InjectRepository 은 NestJS의 Dependency Injection(의존성 주입) 시스템에서 TypeORM 레포지토리를 주입하는 데 사용됩니다. 이는 레포지토리를 서비스 클래스에 주입하여 데이터베이스 작업을 수행할 수 있게 합니다.

private : 접근 제한자는 클래스의 멤버가 해당 클래스 내부에서만 접근 가능하도록 합니다. 즉, 클래스 외부에서는 접근할 수 없습니다.

readonly : 속성 지정자는 클래스 멤버가 읽기 전용임을 명시합니다. 이는 초기화 이후에 값을 변경할 수 없도록 합니다. readonly는 주로 필드가 생성자에서 초기화된 이후 변경되지 않도록 보장하기 위해 사용됩니다

private async hashPassword(password:string){
        try{
            return bcrypt.hash(password,10);
        } catch (error){
            throw new InternalServerErrorException('비밀번호 해싱중 오류가 발생하였습니다')
        }
    }

Node를 사용해 회원가입을 만들었던 경험이 있다면 해싱에 대한 개념을 알고있을거라고 생각합니다.
해싱이란, 말그래도 주어진 데이터를 암호화 하는 작업을 말합니다
말그대로 데이터를 암호화 해서 저장하기 때문에 비밀번호 와 같은 민감한 데이터를 보다안전하게 
저장할수 있다고 볼수 있습니다.

그리고 이렇게 따로 관리한 이유는 추후 실제 회원가입 로직을 구성할때 보다 깔끔하고, 
가독성이 좋게 작성하기위해 분리하여 정의했습니다.

마지막으로 try catch 로 묶어, 에러가 발생했다면 디버깅 하기 쉽게만들기 위해서 작성했습니다.

async signUp({email, password, confirmPassword, username}: SignupDto){
        if(password !== confirmPassword){
            throw new BadRequestException('비밀번호가 일치하지 않습니다.')
        }
        try{
            const existUser = await this.userRepository.findOneBy({email})
            if(existUser){
                throw new BadRequestException('사용중인 이메일입니다.')
            }
            const hashedPassword = await this.hashPassword(password)
            const user = this.userRepository.create({
                email,
                password: hashedPassword,
                username,
            });
            await this.userRepository.save(user)
            return{ email: user.email}
            } catch (error){
            if(error instanceof BadRequestException){
                throw error
            }
            throw new InternalServerErrorException('회원가입중 오류가 발생하였습니다')
        }
    }

signUp이라는 회원가입 API 에 대한 서비스의 핵심로직입니다.

회원가입시 Json 타입으로 바디에 입력한 password 와 confirmPassword 가 일치하지 않다면
에러를 리턴하게 만들었습니다.
(예외처리는 최대한 생각나는 만큼 만들고 또 만들어야 실제서비스에서 버그를줄입니다,
하지만 TIL 작성용이기때문에 간결하게 넘기겠습니다)

existUser 는 실제 User Repository 에 저장되어 있는 데이터중 
바디로 입력한 email 과 동일한 email이 있는지를 확인하기 위해 
선언 해주었습니다.
동일한 이메일로 여러계정을 만들수 없게끔 하고싶어
만약에 동일한 이메일이 존재한다면 BadRequestException 을 던지도록
만들었습니다.

그다음 위에서 말했듯이 안전하게 비밀번호를 저장하기위해 해싱작업을 진행해주고
userRepository 에 저장할 데이터들을 create을 통해 생성해주고

async signUp({email, password, confirmPassword, username}: SignupDto)

로 선언해주었던 파라미터들을 활용해서
email: email
password: hashedPassword
username: username
형식에서 객체분해할당을 이용해

                email,
                password: hashedPassword,
                username,

식으로 작성해주었습니다.

마지막으로  save를 통해 데이터를 저장해주고 
리턴을 해줍니다.

실제 인섬니아 테스트결과 창입니다.