본문 바로가기
Back-end/기초부터 따라하는 nest.js

#8. 기초부터 따라하는 Nest.js : TypeORM을 이용한 간단한 API작성

by hsloth 2023. 4. 17.

 

해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다.
모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다.

 

#pre. 터미널을 켜고 프로젝트 폴더로 이동

https://suloth.tistory.com/44

 

#0-1. 기초부터 따라하는 nest.js : 터미널 키는 법 + 터미널에서 작업 폴더 이동

윈도우 윈도우는 윈도우+R 버튼을 누른 후, cmd 를 입력하여 터미널을 킵니다. 혹은 윈도우 버튼을 눌러서 검색창에 cmd를 검색하면 터미널이 나올텐데 그걸 실행시켜주시면 됩니다. Mac OS Mac의 경

suloth.tistory.com

 
위의 링크의 내용을 참고하여 study 폴더로 이동해줍니다.
그리고 code . 명령어를 통해 vscode를 열어줍니다.


이전 포스팅까지 어느정도 프로젝트 세팅이 끝났습니다.

https://suloth.tistory.com/66

 

#7. 기초부터 따라하는 Nest.js : cross-env를 이용한 scripts에서의 환경변수 관리

해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다. 모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다. #pre. 터미널을 켜고 프로젝트 폴더로 이동 ht

suloth.tistory.com

 

이번 포스팅에서는 TypeORM을 이용하여 간단한 API를 작성해보겠습니다.

 

Module, Controller, Service를 이번 포스팅에서 다룰 예정이니, 혹시 개념이 헷갈리신다면 다음 포스팅을 참고하시기 바랍니다.

https://suloth.tistory.com/47?category=1096080 

 

#2. 기초부터 따라하는 Nest.js : HTTP 메소드와 Nest.js 구조

해당 포스팅은 nest.js 9.0.0 버전을 기준으로 작성되었습니다. 모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다. HTTP 메소드 HTTP 메소드는 사용자가 백엔드 서버에게 무언가

suloth.tistory.com


회원가입 API

이번 포스팅에서는 회원가입 API를 작성해보도록 하겠습니다.


UserController


먼저, UserController에서 함수를 작성해줍니다. 

Controller는 사용자가 요청한 URL에 맞게 Service를 제공하는 역할을 합니다.

 

import { Controller, Get, Post } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('/user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  ...

  @Post('register')
  async register() {
    return this.userService.register()
  }

}

@Post('register')일단, 회원가입은 User를 DB에 추가(Create, 등록)하는 함수이므로 HTTP Method 중 Post 메소드를 사용합니다. 그리고 그 경로는 /user/register 로 설정해 줍니다. 그리고 이 데코레이터는 바로 아래 함수인 register()를 꾸며줍니다.

 

async register() : register란 이름의 비동기 함수를 정의합니다.

 

return this.userService.register() : 여기서 this는 UserController를 가리킵니다. 따라서, contructor에서 주입해주었던 userService의 register함수를 사용한다는 뜻입니다. (UserController의 register함수를 사용하는게 아닙니다)

나중에 UserService에서 선언한 register함수의 결과 값이 this.userService.register()에 할당됩니다. 아마 지금은 UserService에서 register라는 함수를 만들기 전이기 때문에 register() 부분이 빨간색으로 오류가 날 겁니다.

 


UserService


Controller에서 함수 작성을 마친 후, UserService에서 서비스를 제공할 함수를 만들어줍니다.

 

그전에, Repository를 Service에 주입해주어야 합니다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
  ) {}

	...
}

참고로, 저는 Repository를 한 번 더 나누어서 사용하는 편입니다. 쿼리의 재사용성.. 때문이라고 하는데... 복잡한 쿼리를 Repository로 짜두면, 나중에 비슷한 쿼리가 필요할 때 함수 하나만 사용해서 작성할 수 있습니다.

일단 그냥 따라합시다.

@InjectRepository(UserEntity) : UserEntity에 대한 Repository를 주입한다고 알리는 데코레이터입니다.

private readonly userRepository: Repository<UserEntity> : UserEntity에 대한 Repository를 userRepository라는 변수명으로 정의합니다. 그냥 외웁시다!

 

그리고 서비스를 제공할 함수를 만들어봅시다.

 

우리가 전에 만들었던 DB구조입니다.

 

 

위의 그림을 보면, email과 password가 필요하다는 걸 알 수 있습니다.

따라서, 함수의 인자로 email과 password를 받아야겠죠.

그리고 이 email과 password를 이용해 DB에 유저를 등록해야 합니다. 이 때, 위에서 Inject한 userRepository를 사용합니다.

그래서 간단하게는 다음과 같이 코드를 작성할 수 있습니다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
  ) {}

	...

  async register(email: string, password: string) {
    const user = await this.userRepository.save({
      email: email,
      password: password,
    });

    return user;
  }
}

async register(email: string, password: string) : string타입인 email과 password를 인자로 받는 register함수를 정의합니다.

 

const user = await this.userRepository.save({ email: email, password: password }) : userRepository는 DB에 쿼리문을 날려주는 아이입니다. 이해하기 어렵다면, userRepository는 DB와 소통할 수 있게 해주는 아이라고 생각하면 됩니다. 그래서 이 userRepository의 save라는 함수를 이용해서 User 테이블에 email과 password를 save(create)한다고 생각합시다.

앞의 await는 이 함수는 비동기로 작동하기 때문에 await를 걸어서 DB에 쿼리문이 날아가서 결과값을 받아올 때까지 기다리기 위해 필요합니다. 그렇지 않고 await없이 this.userRepository.save를 사용하면, user에는 Promise라는 엉뚱한 값이 담기게 됩니다.

 

return user; : save함수는 create(혹은 update)와 select를 동시에 합니다. 그래서 user 변수에, 생성한 user에 대한 정보를 담아주기 때문에 user를 그대로 리턴합니다. 참고로, save함수는 이미 해당 데이터가 존재한다면 create가 아닌 update문을 날립니다.

만약 이 말이 이해가 안되신다면, MySQL에 대해 공부를 조금 더 할 필요가 있으실 것 같습니다.

 

Bcrypt

하지만, 여기서 DB에 패스워드를 그대로 저장하게 된다면, 많은 문제가 생길 수 있습니다. 비밀번호가 도중에 탈취된다던지... 아니면 해킹당했을 경우 DB에서 쌩 비밀번호가 그대로 노출이 되기 때문에... 이것을 방지하기 위해 비밀번호는 항상 암호화를 해서 저장을 합니다.

그리고 이 때 사용되는 라이브러리가 바로 Bcrypt입니다.

 

먼저, bcrypt를 설치해줍니다. 터미널에서 프로젝트 폴더 최상위(혹은 package.json이 있는 폴더)로 이동한 뒤 다음 명령어를 입력해줍시다.

npm i bcrypt

 

그리고 bcrypt를 이용해 password를 다음과 같이 암호화 합시다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/user.entity';
import { Repository } from 'typeorm';
import { hash } from 'bcrypt';  // 이부분이 추가되었습니다.

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
  ) {}

	...

  async register(email: string, password: string) {
    const hashedPassword = await hash(password, 10); // 10은 salt값으로 암호화할 때, 필요하다.

    const user = await this.userRepository.save({
      email: email,
      password: hashedPassword,
    });

    return user;
  }
}

const hashedPassword = await hash(password, 10) : bcrypt의 hash함수를 이용하여 password를 암호화된 password로 바꿔줍니다. 여기서 10은 salt값으로, 암호화시 추가해주는 사용자가 지정한 임의의 값입니다. 사용자가 지정한 salt값에 따라 암호화된 값이 완전히 달라지므로, 지금은 10으로 썼지만 나중에 configService를 이용해서 감춰주는게 좋습니다. 

그리고 userRepository를 이용하여 password: hashedPassword하여 쌩password대신, 암호화된 password를 DB에 저장하도록 합시다.

 

여기서 await를 사용한 이유를 추가로 설명하자면,

hashedPassword가 있어야 userRepository.save함수로 hashedPassword를 DB에 저장할 수 있고, DB에 저장을 해서 user 정보를 받아야 return user를 통해 유저를 리턴할 수 있습니다.

await가 없으면 값을 못받은채로 (값이 null인채로) 다음코드로 넘어간다고 생각하시면 됩니다.

 

일단, 여기까지하면 끝입니다만... 서비스를 작성하면 고려해야할게 많습니다. 조금 더 코드를 보완해보자면

 

이미 존재하는 유저

이미 유저가 존재하면 회원가입이 불가능해야 합니다. 그러므로 코드를 다음과 같이 수정할 수 있습니다.

import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/user.entity';
import { Repository } from 'typeorm';
import { hash } from 'bcrypt';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
  ) {}

	...

  async register(email: string, password: string) {
    const existedUser = await this.userRepository.findOne({
      where: {
        email: email,
      },
    });

    if (existedUser) {
      throw new BadRequestException('이미 해당 이메일이 존재합니다.');
    }

    const hashedPassword = await hash(password);

    const user = await this.userRepository.save({
      email: email,
      password: hashedPassword,
    });

    return user;
  }
}

const existedUser = await this.userRepository.findOne({ where: { email: email } }) : userRepository의 findOne함수를 이용해서 email칼럼의 값이 email변수에 담긴 이메일과 같은 user를 select합니다. (findOne함수는 이름 그대로 하나의 user를 찾는 함수입니다)

만약 existedUser에 값이 담기지 않는다면 (undefined 혹은 null 이라면) 중복 유저가 없다는 소리겠죠.

 

if (existedUser) { throw new BadRequestException('이미 해당 이메일이 존재합니다.'); } : 만약 existedUser가 존재한다면, BadRequestException이라는 에러를 던집니다(throw). throw문은 return과 동일하다고 보면됩니다. 다만, 값을 다루는 return과 달리, throw는 에러를 다룹니다. 따라서 throw문이 실행되면 함수에서 탈출하기 때문에 뒤의 코드는 실행되지 않습니다. 그래서 userRepository.save함수는 실행이 되지 않게 됩니다. 이것으로 중복유저를 생성하는 일을 막을 수 있겠죠.

 

그리고 모든 에러에는 해당 에러에 추가로 메시지를 덧붙여서 에러를 던질 수 있습니다. 위의 코드의 경우에는 해당 이메일이 이미 존재하는 경우에 에러를 던지기 때문에 "이미 해당 이메일이 존재합니다." 라는 메시지를 함께 던져주었습니다.

 

 


다시, UserController로


우리가 위에서 작성한 코드를 다시 봐봅시다.

import { Controller, Get, Post } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('/user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get('/main')
  async getMainPage() {
    return this.userService.getMainPage();
  }

  @Post('register')
  async register() {
    return this.userService.register()
  }

}

Post메소드에는 항상 body가 딸려옵니다. body란, 프론트가 Post메소드를 요청하면서 함께 날리는 내용입니다.

여기서는 email과 password가 body에 들어있다고 볼 수 있습니다. 따라서, 다음과 같이 코드를 바꾸겠습니다.

import { Body, Controller, Get, Post } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('/user')
export class UserController {
  constructor(private readonly userService: UserService) {}

	...

  @Post('register')
  async register(@Body() body) {
    const email = body?.email;
    const password = body?.password;

    return this.userService.register(email, password);
  }
}

@Body 데코레이터를 이용해 req.body에 담긴 내용을 가져옵니다. (여기서 req는 request, 요청이라는 뜻으로 프론트에서 보내는 데이터를 담은 변수입니다. 이 변수 내부에 body가 있습니다)

그래서 body.email과 body.password로 이메일과 패스워드를 뽑아서 service에 전달할 수 있게 됩니다.

 


UserModule


마지막으로, UserModule에 TypeORM에서 가져가 사용할 Entity를 등록해줍니다.

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from 'src/entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

AppModule에서 이미 forRoot로 기본적인 세팅을 해주었으니, UserModule에서는 forFeature로 어떤 엔티티를 사용할 것인지만 등록해주면 됩니다.

만약 오류가 난다면 AppModule을 한 번 봐봅시다. 다음과 같이 설정되어 있어야 합니다.

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as path from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './res/user/user.module';
import { ConfigModule, ConfigService } from '@nestjs/config';

console.log(`.env.${process.env.NODE_ENV}`);

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.env.${process.env.NODE_ENV}`,
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        retryAttempts: configService.get('NODE_ENV') === 'prod' ? 10 : 1,
        type: 'mysql',
        host: configService.get('DB_HOST'),
        port: Number(configService.get('DB_PORT')),
        database: configService.get('DB_NAME'),
        username: configService.get('DB_USER'),
        password: configService.get('DB_PASSWORD'),
        entities: [path.join(__dirname, '/entities/**/*.entity.{js, ts}')],
        synchronize: false,
        logging: true,
        timezone: 'local',
      }),
    }),
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 


PostMan


다음은 postman이라는 프로그램을 실행시켜서 Post메소드를 날려보겠습니다.

 

먼저, Postman을 실행시키고 다음과 같이 설정해줍시다.

 

 

위의 빨간 동그라미 부분이 Json으로 되어있어야 합니다.

 

그리고 Send 버튼을 누르면 body에 담긴 data를 보냅니다.

 

 

그리고 Send가 완료되면 아래에 응답 Data를 받아옵니다.

password를 보시면 암호화가 되어있는걸 알 수 있습니다.

 

그리고 같은 email과 password가 담긴 post요청을 다시 보낸다면, 아래와같이 '이미 해당 이메일이 존재합니다'라는 메시지가 옵니다.

 

터미널을 보면 다음과 같이 자동적으로 작성되는 쿼리문을 볼 수 있습니다.


이제 밑에 글을 참고하여 github에 코드를 push합시다.

 

https://suloth.tistory.com/46

 

#0-2. 기초부터 따라하는 Nest.js : Git과 Github 사용법

이 글은 아래의 포스팅에 이어서 작성하는 포스팅입니다. https://suloth.tistory.com/45 #1. 기초부터 따라하는 Nest.js : Nest.js 초기 설정 Nest.js란? node.js의 백엔드 프레임워크 중 하나입니다. node.js에는 수

suloth.tistory.com

 

아래는 참고하시라고 올려두는 사진입니다.