NestJS 개발 시작하기(로그인 인증 개발); 4. TypeORM으로 DB연결
NestJS 개발 시작하기(로그인 인증 개발)
4. TypeORM으로 DB연결
5. TypeORM
앞에서 정의한 Account정보를 DB로 저장하기 위해, 가장 많이 사용하고 있는 DB인 MySQL을 사용하기로 했다. PostgreSQL이나, Node.js와 같이 사용하기 쉬운 MongoDB도 사용하는 방법은 많이 다르지 않다. MySQL과 연결하고 데이터를 저장하기 위해서는 mysql패키지를 설치하고 DB를 직접 연결하는 방법도 있지만, NestJS와 가장 많이 사용하는 방법은 TypeORM을 사용하는 방법이다.
노트북에 개발용 MySQL이 없다면 아래 링크를 참고해서 MySQL을 실행한다.
윈도우에 만드는 리눅스 개발 환경; 5. Docker로 MySQL시작하기
윈도우에 만드는 리눅스 개발 환경 목차1. 우분투 리눅스 설치 (윈도우 10)2. 윈도우에 Docker 설치3. 속도 개선 후 node.js 설치4. VSCode 설치 후 리눅스 연결5. Docker로 MySQL시작하기6. Docker로 MongoDB 시작
front-it.tistory.com
a. MySQL을 연결
- 먼저 필요한 패키지들을 설치한다.
pnpm install @nestjs/typeorm typeorm mysql2 @nestjs/config
설치가 끝나면 MySQL에 auth개발에 필요한 데이터베이스와 사용자를 만들기 위해 mysql에 연결한다. Docker로 mysql을 실행하고 있으면 docker서버에 직접 들어가서 실행하면 된다.
docker exec -it mysqldb /bin/bash
# 접속이 되면
mysql -uroot -p
접속이 되면 아래와 같이 SQL문을 사용해서 데이터베이스와 사용자를 생성한다. 아래의 아이디와 암호는 글을 작성하기 위해 지정한 것이기 때문에 실제 사용할 때는 바꿔서 문장을 실행하면 된다.
create database authdb;
create user authuser identified by "authpass";
grant all on authdb.* to "authuser"@"%";
이제 auth에 사용할 데이터베이스와 사용자가 만들어졌으니, 프로젝트의 모듈에 mysql연결 정보를 설정한다. 먼저 .env파일을 프로젝트의 최상위 위치에 만들고 아래와 같이 DB연결에 필요한 정보를 저장한다. 이 정보에는 계정정보를 포함하고 있기 때문에 프로그램에 직접 기록하기보다는 .env 파일에 기록하고 .gitignore에 등록하는 것이 보편적이다.
DB_HOST='192.168.59.102'
DB_USER='authuser'
DB_PASSWORD='authpass'
DB_DATABASE='authdb'
.env파일의 정보는 ConfigModule을 이용해 TypeOrmModule로 전달하면 TypeOrmModule에 직접 아이디, 패스워드를 넣지 않고 사용할 수 있다. TypeOrmModule은 DB를 사용할 모듈에서 정의해도 되지만, auth는 최상위 모듈인 app에 선언하기로 하고 app.module.ts파일을 아래와 같이 수정했다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Account } from './auth/entities/account.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST'),
port: 3306,
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [Account],
synchronize: true
}),
inject: [ConfigService]
}),
AuthModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
b. TypeORM으로 Service를 rebuild
DB연결을 설정했으니, 이제 DB에 저장할 내용을 Entity로 정의할 차례이다. TypeORM도 NestJS처럼 CLI를 제공하고 있기 때문에, TypeORM을 global에 패키지로 설치했으면 typeorm명령을 사용할 수 있다. TypeORM 공홈 문서에 따르면 entity를 만들기 위해
typeorm entity:create path-to-entity-dir/entity
명령을 사용하라고 되어 있는데, 사용해서 만들어도 사실 @Entity로 쌓인 class파일을 하나 만들어 주는 것 이외에는 없기 때문에 그냥 만들어도 관계없다. Entity를 저장하기 위해 entities폴더를 auth밑에 하나 만드는 것이 좋다.
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Account {
@PrimaryGeneratedColumn("uuid")
id: number;
@Column()
email: string;
@Column()
password: string;
@Column()
name: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}
account.module.ts파일에도 TypeOrmModule을 import 하고 Account를 사용할 수 있도록 전달한다.
import { Module } from '@nestjs/common';
import { SignController } from './sign/sign.controller';
import { SignService } from './sign/sign.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Account } from './entities/account.entity';
@Module({
imports: [TypeOrmModule.forFeature([Account])],
controllers: [SignController],
providers: [SignService]
})
export class AuthModule {}
마지막으로 account.service.ts를 아래와 같이 account.interface.ts에 정의된 Account interface대신 account.entity.ts에 정의한 Account로 바꾸고 constructor를 사용해 Repository 클래스를 DI 해서 클래스 내부의 method들이 사용할 수 있도록 한다. 이제 앞에서 Account interface를 사용해 클래스 내에서 관리하던 부분을 모두 Repository API를 사용해 바꾼다.
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Account } from '../entities/account.entity';
import { SignUpDto } from '../dto/SignUpDto';
@Injectable()
export class SignService {
isLogedin = {};
constructor(@InjectRepository(Account) private repo: Repository<Account>) { }
async find(email: string): Promise<Account> {
return this.repo.findOne({ where: { email: email } });
}
async createAccount(account: SignUpDto) : Promise<boolean> {
const existingAccount = await this.find(account.email);
console.log(existingAccount);
if (existingAccount == null) {
const newAccount = this.repo.create(account);
this.isLogedin[account.email] = false;
this.repo.save(newAccount);
return true;
}
return false;
}
async login(email: string, password: string): Promise<boolean> {
const existingAccount = await this.find(email);
if (existingAccount != null && existingAccount.password === password) {
this.isLogedin[email] = true;
return true;
}
return false;
}
async logout(email: string): Promise<boolean> {
const existingAccount = await this.find(email);
if (existingAccount != null) {
this.isLogedin[email] = false;
return true;
}
return false;
}
}
Repository API는 대표적으로 아래와 같은 5개의 함수를 갖는다.
create() | Entity를 만든다. DB에 저장하지는 않는다. |
save() | DB에 저장한다. 필요하면 생성한다. |
find() | 검색을 하고 Entity의 배열을 돌려준다. 결과가 없으면 [] |
findOne() | 검색을 하고 Entity를 돌려준다. 결과가 없으면 null |
remove() | DB에서 레코드를 삭제한다. |
Entity를 만들때에도 new로 생성하지 않고 Repository API의 create()를 사용하면 Entity에 Validation이나 @AfterInsert등의 어노테이션을 사용해서 Logging등의 추가 작업을 할 수 있기 때문에 JavaScript Object를 바로 만들지 말고 Repository의 create()를 사용하는 것이 좋다.
...
@AfterInsert()
loggin() {
console.log("inserted.")
}
}
마지막으로, Controller도 Service수정에 맞춰 일부 수정하고, HTTP status도 같이 보내도록 추가했다.
import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
import { SignUpDto } from '../dto/SignUpDto';
import { SignService } from './sign.service';
@Controller('sign')
export class SignController {
constructor(private readonly signService: SignService) {}
@Post('up')
async signUp(@Body() signUpDto: SignUpDto, @Res() res) {
console.debug('Sign up\n' + 'SignUpDto = ' + JSON.stringify(signUpDto));
const result = await this.signService.createAccount(signUpDto);
return res.status(result ? HttpStatus.CREATED : HttpStatus.CONFLICT).send('Sign up ' + (result ? 'success' : 'failed'));
}
@Get('in/:id')
async signIn(@Param('id') id: string, @Query('password') password: string, @Res() res) {
console.debug('Sign in\n' + 'id = '+ id + ', password = ' + password);
const result = await this.signService.login(id, password);
return res.status(result ? HttpStatus.OK : HttpStatus.UNAUTHORIZED).send('Sign in ' + (result ? 'success' : 'failed'));
}
@Get('out/:id')
async signOut(@Param('id') id: string, @Res() res) {
console.debug('Sign in\n' + 'id = '+ id);
const result = await this.signService.logout(id);
return res.status(result ? HttpStatus.OK : HttpStatus.UNAUTHORIZED).send('Sign out ' + (result ? 'success' : 'failed'));
}
}