지난 포스트
[NestJS 기반 게시판 REST API 만들기] 2. 회원가입 및 로그인 기능을 위한 DB 연동
지난번 포스트에서는 데이터베이스를 연동하고, 유저 테이블을 만들고, 엔티티 클래스를 만들어 연동까지 했습니다.
이번 시간에는 회원가입 API를 만들어 유저 테이블에 직접 데이터를 넣어보고, 로그인 API를 통해 로그인 기능까지 구현해보겠습니다. 또한 입력 파라미터를 검증하는 과정과 에러 처리과정을 보여드리겠습니다.
1. UserService 구현하기
이전 포스트에서 만들어두기만 했던 UserService를 회원가입과 로그인 처리를 위해 구현해보도록 하겠습니다.
가장 먼저 사용자 패스워드를 암호화 하기 위하여 bcryptjs 라이브러리를 추가합니다. 사용자 비밀번호가 평문으로 저장되어있다면 법적으로 문제가 되니, 무조건 암호화해주셔야 하며 암호화 방식은 SHA 256 , SHA 512, bcrypt와 같은 해쉬 함수들을 많이 사용합니다. 저는 사용자 패스워드 암호화 목적으로 설계된 bcrypt를 선택했습니다. 또한 데이터 검증을 위해 Joi 라이브러리도 추가합니다.
yarn add bcryptjs joi
yarn add -D @types/bcryptjs @types/joi
두 번째로는 앞서, User 엔티티를 조작하기 위해 만들어둔 UserRepository를 UserService에 주입하여 사용하기 위해 UserModule을 다음과 같이 작성합니다.
[ user.module.ts ]
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './user.controller';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';
@Module({
imports: [TypeOrmModule.forFeature([UserRepository])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
NestJS에서 제공하는 @nestjs/typeorm 라이브러리는 nestjs의 환경에 맞게 typeorm을 사용할 수 있도록
지원해줍니다. TypeOrmModule.forFreature() 함수의 파라미터로는 사용할 Entity 리스트 또는 Repository 리스트를 넣어줍니다.
이후 다음과 같이 UserModule의 Provider인 UserService에선 Import 된 UserRepository를 생성자의 파라미터로 받아 사용할 수 있습니다.
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository){}
}
이제 회원가입과 로그인 기능을 구현하기 앞서, 먼저 유저 회원가입과 로그인에 필요한 데이터들의 타입을 지정합니다.
[user.type.ts]
export type Login = {
email: string;
password: string;
}
export type Register = {
email: string;
name: string;
password: string;
}
export type UserInfo = {
uuid: string;
email: string;
name: string;
}
또한 API가 요구하는 데이터 파라미터를 client가 제대로 전송하였는지 검증하기 위하여, Joi 라이브러리를 사용하여 회원가입과 로그인 API의 데이터 스키마를 작성합니다(여러 라이브러리가 있지만 저는 Joi를 사용하였습니다.)
[user.schema.ts]
import * as Joi from 'joi';
export const registerSchema = Joi.object({
email: Joi.string().required(),
name: Joi.string().required(),
password: Joi.string().required()
})
export const loginSchema = Joi.object({
email: Joi.string().required(),
password: Joi.string().required()
})
API 요청의 응답의 구조는 변경되지 않아야 하며, 데이터가 추가되어도 Client 단에서 기존 코드를 유지한 체 데이터를 가져올 수 있어야 합니다. (이와 같은 내용은 추후 포스트에 자세하게 설명하도록 하겠습니다.)
그래서 저는 일정한 구조로 응답 메시지를 만들어내는 Response Util을 사용하여 Controller의 응답을 처리합니다.
[response.util]
/**
* Response Message Builder
*/
export class ResponseMessage {
private data: any | any[]; // response data
private code: number; // response code
public success(): ResponseMessage {
this.code = 1;
return this;
}
public error(code: number, message: string = "Error"): ResponseMessage {
this.code = code;
this.data = {message};
return this;
}
public body(data: any | any[] = ''): ResponseMessage {
this.data = data;
return this;
}
get Data(): any | any[] {
return this.data;
}
get Code(): number {
return this.code;
}
public build(): Response{
return new Response(this);
}
}
export class Response {
data: any | any[];
code: number;
constructor(message: ResponseMessage) {
this.data = message.Data;
this.code = message.Code;
}
}
위와 같은 클래스를 통해 API의 응답은 항상 다음과 같은 구조를 가지게 됩니다.
{
"code":1, // 정상적으로 처리된 경우 1로 반환, 비정상처리의 경우 약속된 에러코드를 반환
"data":{
// 모든 데이터는 이 안에 넣어져서 처리됨, 내부에 데이터가 추가되거나 삭제되어도 클라이언트는
// "data" 라는 프로퍼티를 파싱하면 수정된 데이터 이외 기존 데이터를 사용하는 코드들은
// 수정될 필요없이 정상적으로 수행되는 효과가 있음
}
}
이제 http POST 메소드를 처리하는 @Post() 데코레이터와 Request Body 내용을 반환하는 @Body() 데코레이터를 활용하여 API 요청 데이터를 받고, user.type.ts 와 user.schema.ts를 통해 파라미터를 검증하도록 하겠습니다.
[user.controller.ts]
import { Body, Controller, Logger, Post } from '@nestjs/common';
import { ValidationError } from 'Joi';
import { Response, ResponseMessage } from '../util/response.util';
import { loginSchema, registerSchema } from './user.schema';
import { UserService } from './user.service';
import { Login, Register, UserInfo } from './user.type';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
@Post("register")
public async addUser(@Body() register: Register): Promise<Response> {
try {
const { value, error }: { value: Register, error?: ValidationError } = registerSchema.validate(register);
if (error) {
Logger.error(error);
return new ResponseMessage().error(999).body("Parameter Error").build();
}
const user: UserInfo = await this.userService.addUser(value);
return new ResponseMessage().success().body(user).build();
} catch (err) {
Logger.error(err);
}
}
@Post('login')
public async login(@Body() login: Login): Promise<Response> {
const { value, error }: { value: Login, error?: ValidationError } = loginSchema.validate(loginSchema);
if (error) {
Logger.error(error);
return new ResponseMessage().error(999).body("Parameter Error").build();
}
const user = await this.userService.login(value);
if (!user) {
return new ResponseMessage().error(999, "Login Error").build();
}
return new ResponseMessage().success().body(user).build();
}
}
[ user.service.ts ]
import { Injectable } from '@nestjs/common';
import { User } from '../entities/user.entity';
import { Login, UserInfo, LoginUserInfo, Register } from './user.type';
import { UserRepository } from './user.repository';
import * as Bcrypt from 'bcryptjs';
import { Token } from 'src/util/token.util';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository){}
public async addUser(register: Register): Promise<UserInfo>{
const registerUser = await this.userRepository.create();
// Encode User Password
const salt: string = await Bcrypt.genSalt(10);
const password: string = await Bcrypt.hash(register.password, salt);
registerUser.email = register.email;
registerUser.name = register.name;
registerUser.uuid = Token.getUUID();
registerUser.password = password;
const user = await this.userRepository.save(registerUser);
const userInfo: UserInfo = {
email: user.email,
name: user.name,
uuid: user.uuid
};
return userInfo;
}
public async login(loginUser: Login): Promise<LoginUserInfo>{
const user: User = await this.userRepository.findOne({
where:{
email: loginUser.email
}
});
const passwordCheck = await Bcrypt.compare(loginUser.password, user.password);
if(!passwordCheck){
return null;
}
user.lastLoginDate = new Date();
await this.userRepository.save(user);;
const userInfo: LoginUserInfo = {
email: user.email,
name: user.name,
uuid: user.uuid,
lastLogin: user.lastLoginDate
};
return userInfo;
}
}
위와 같이 API의 요청 파리미터를 받고, 스키마 검증 이후에 UserService 인스턴스의 addUser 메서드와 login 메서드로 요청 파라미터를 넘기고, UserService에서 회원가입과 로그인 검증을 위한 코드를 작성합니다.
@Injectable() 데코레이터를 붙이면 해당 클래스는 NestJS에서 자체적으로 인스턴스를 만들어 주입할 수 있게 됩니다. 파라미터로는 객체 생성 스코프에 대한 옵션이 있습니다. 기본적으로는 싱글톤 형태로 인스턴스가 생성되어 주입되며 재사용됩니다.
그럼 서버를 구동해보고, 로그를 확인해보겠습니다.
로그를 보시면 UserController에 /register POST 와 /login POST 라우터가 성공적으로 바인딩된 것을 확인할 수 있습니다.
이후 포스트맨을 실행시켜 회원가입 요청을 처리해보고, 로그인을 해보며, 파라미터 에러 예외처리가 제대로 이루어지는지 확인 해보겠습니다.
1. 회원가입 API 요청
회원가입 요청을 성공적으로 처리하여, 유저의 민감한 정보를 제외한 유저 정보가 반환되었습니다.
2. 로그인 API 요청
로그인 요청을 서버에서 받아, 유저 정보와 비밀번호를 확인한 후 유저 정보가 반환되었습니다.
3. API 파라미터 예외처리
로그인 API에서 반드시 들어와야 할 데이터를 누락시켜(ex: 비밀번호) 서버에 요청하면 다음과 같이 에러 메시지와 에러코드를 반화하며 예외처리가 성공적으로 이루어집니다.
위 방법으로 각 컨트롤러에서 각 요청에 따른 서비스의 예외를 처리하면, 클라이언트는 해당 에러 메시지에 대하여 쉽게 예외처리를 할 수 있습니다(ex: 경고창을 띄우는 등).
[깃헙 주소]
'Programming > Nodejs' 카테고리의 다른 글
[NodeJS] APK 파싱 (2) | 2019.06.03 |
---|---|
[NestJS] AWS S3 Image Upload (0) | 2019.06.03 |
NodeJS AES 256 암복호화 코드 (3) | 2019.06.03 |
[NestJS 기반 게시판 REST API 만들기] 2. 회원가입 및 로그인 기능 을 위한 DB 연동 (3) | 2019.04.30 |
[NestJS 기반 게시판 REST API 만들기] 1. 프로젝트 환경 구축 (12) | 2019.04.17 |