NestJS 시작하기

nestJS - cli 설치

사전 조건 : node lts버전 설치

1
npm  i @nestjs/cli

 

또는

1
npm i -g @nestjs/cli

 

https://i.imgur.com/j8evD5H.png  

이렇게 package.json 파일에서 정상적으로 설치되었음을 확인할 수 있다.
 

nestJS - new Project 시작하기

1
2
3
4
nest new <new project name>

// 예시
nest new nestjs-test

위와 같이 입력하면 현재 터미널이 위치한 폴더 내에 새로운 “nestjs-test"폴더가 만들어지면서 그 안에 세팅이 된다.
 

만약 $ mkdir nestjs-test 로 폴더를 만들고 이 위치에서 진행중이라면,

1
nest new ./

위와 같이 입력시 현재 폴더명 그대로 새로운 프로젝트가 진행된다.
 

계속해서 진행시 아래와 같은 선택을 묻는다.

https://i.imgur.com/bhFD5k1.png  

npm 또는 yarn 등, 편한 패키지로 선택하면
nestJS 기본 세팅이 이루어진다.
 

https://i.imgur.com/vvzzzBK.png  

위 이미지와 같이 prettier부터 controller-service-model, main.ts, jest 등 기본적인 구성이 모두 세팅된다. (시간이 조금 걸릴 수 있다.)


모듈 생성

nestJS에서는 nest 명령어로 모듈을 생성할 수 있다.

1
nest g module boards

 

설명

nest : nest cli 사용 g : generate module : schematic that I want to create boards : name of schematic

 

controller, service 생성하기

1
2
nest g controller boards --no-spec
nest g service boards --no-spec

한줄씩 차례로 입력하면 된다.
 

설명

nest : using nest cli g : generate controller : controller schematic service : service schematic boards: name of the schematic –no-spec: 테스트를 위한 소스코드 생성을 하지 않겠다!

Tip

접근제한자를 이용해서 소스 간단하게하기 접근제한자(public,protected,private)을 생성자(constructor) 파라미터에 선언하면 접근제한자가 사용된 생성자파라미터는 암묵적으로 클래스 프로퍼티로 선언된다.

 
 

NestJS에서 request의 body값은 어떻게 가져올까?

NodeJS에서는 req.body로 가져올 수 있다.

1
2
3
app.post(`/`, (req,res) => {
	console.log(req.body);
})

 

하지만 NestJS에서는 @Body body를 이용해서 가져온다. 이렇게 하면 모든 request에서 보내온 값을 가져올 수 있으며, 만약, 하나씩 가져오려면 @Body('title') title 혹은 @Body('description') description 이런식으로 가져올 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Post()
createBoard(@Body() body) {
	console.log('body', body);
}

@Post()
createBoard(@Body('title') title: string,
		   @Body('description'), description: string,
		   ) {
	console.log('title', title);
	console.log('description', description);
}

 
 

NestJS에서 Param은 어떻게 가져올까?

localhost:3000/:id 와 같은 params 값의 경우, body와 유사하다.
위의 경우에는 findOne(@Param('id') id: string) 과 같이 가져오면 된다.
만약 여러개의 Param을 가져와야 한다면, findOne(@Param() params: string[]) 으로 가져올 수 있다.
 

Note
@는 데코레이터라고 부른다.
타입스크립트의 데코레이터는 파이썬의 데코레이터나 자바의 어노테이션과 유사한 기능을 한다.
클래스, 메서드, 접근자, 프로퍼티, 매개변수에 적용 가능
참고 - 2.6 데코레이터 - NestJS로 배우는 백엔드 프로그래밍

DTO (Data Transfer Object)란?

계층간 데이터 교환을 위한 객체
DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용하는 객체를 말한다.
DTO는 데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체이다.

NestJS 공식문서에서는 interface보다는 class를 이용해서 정의하는 것을 추천하고 있다.

DTO(Data Transfer Object)를 쓰는 이유는?

  • 데이터 유효성을 체크하는데 효율적
  • 더 안정적인 코드로 만들어준다. 타입스크립트의 타입으로도 사용된다.

예시) controller, service에서 인자로 title, description이 있는 상황에서 만약 title을 다른 인자로 바꾼다면?

  • 인자가 하나인 경우에는 크게 문제가 없겠지만 이것들이 여러개라면 모든 파일에서 인자를 다 수정해줘야 한다.
  • 하지만 DTO를 이용한다면 이 모든 것을 하나의 class로 처리하고 해당 class만 수정하면 된다.

Pipe란??

파이프는 @Injectable() 데코레이터로 주석이 달린 클래스이다.
파이프는 data transformation (데이터 변형)과 data validation (데이터 유효성)을 위해서 사용된다.
파이프는 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.
Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이트는 메소드로 향하는 인수를 수신하고 이에 대해 작동한다.

Data Transformation?

입력 데이터를 원하는 형식으로 변환 (예 : 문자열을 정수로)

PIPE 사용하는 법 (Binding Pipes)

  1. Handler-level Pipes
  2. Parameter-level pipes
  3. Golbal-level Pipes

Built-in PIpes

NestJS에서 기본적으로 사용할 수 있는 6가지 파이프

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuesPipe

파이프를 이용한 유효성 체크 검사

필요한 모듈

  • class-validator, class-transformer
  • npm install class-validator class-transformer --save
  • Documentation page
예시

create-board.dto.ts 파일에서

1
2
3
4
5
6
7
8
 import { IsNotEmpty } from 'class-validator';  
  
export class CreateBoardDto {  
  @IsNotEmpty()  // <-- 이렇게
  title: string;  
  @IsNotEmpty()  // <-- 이렇게
  description: string;  
}

 

먼저 위와 같이 각 인자별로 반드시 있어야 하는 곳에 @IsNotEmpty를 적용시킨다.
 

controller 파일에서

1
2
3
4
5
@Post()
  @UsePipes(ValidationPipe)  // <-- 이렇게
  createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardsService.createBoard(createBoardDto);
  }

 

다음으로 위와 같이 해당 부분에 @UsePipes(ValidationPipe)를 적용시킨다.
 

위 방법은 앞서 언급했던 3가지 Binding Pipes 중, Handler-level Pipes이다.
 

@IsNotEmpty를 적용하지 않았을 경우 실제 Postman을 돌려보면 UsePipes에서 해당 함수를 찾을 수 없다는 오류가 뜨며, @UsePipes를 적용하지 않았을 경우에는 해당 유효성 검사 자체가 이루어지지 않는다.
 

에러를 표출해주기 위해서는

예를 들어 찾는 게시물이 없는 경우, 예외 인스턴스를 생성해서 이용할 수 있다.

1
2
3
4
5
6
7
8
9
getBoardById(id: string): Board {
	const found = this.boards.find(board => board.id === id);

	if(!found) {
		throw new NotFoundException()
	}
	
	return found;
}

 

NotFoundException은 nestjs에 있는 인스턴스이다. (import로 사용가능) 사용시 다음과 같은 에러메세지를 볼 수 있다.

1
2
3
4
{
    "statusCode": 404,
    "message": "Not Found"
}

 

여기서 만약 에러메세지를 따로 넣어주고 싶다면,

1
2
3
4
5
6
7
8
9
getBoardById(id: string): Board {
	const found = this.boards.find(board => board.id === id);

	if(!found) {
		throw new NotFoundException(`Can't find Board with id ${id}`)
	}
	
	return found;
}

 

이렇게 하면, 아래와 같이 response 된다.

1
2
3
4
5
{
    "statusCode": 404,
    "message": "Can't find Board with id wefewwefewf",
    "error": "Not Found"
}

 

커스텀 파이프 구현방법

먼저 Pipe Transform이란 인터페이스를 새롭게 만들 커스텀 파이프에 구현해줘야 한다.
이 Pipe Transform 인터페이스는 모든 파이프에서 구현해줘야 하는 인터페이스이다.
그리고 이것과 함께 모든 파이프는 transform()메소드를 필요로 한다.
이 메소드는 NestJS가 인자(arjuments)를 처리하기 위해서 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { ArgumentMetadata, PipeTransform } from "@nestjs/common"

export class BoardStatusValidationPipe implements PipeTransform {
	transfomr(value: any, metadata: ArgumentMetadata) {
		console.log('value', value)
		console.log('metadata', metadata)
	
	return value;
	}
}

 

transform() 메소드

이 메소드는 2 개의 파라미터를 가진다.

  1. 첫번째 파라미터는 처리가 된 인자의 값(value)이며,
  2. 두번째 파라미터는 인자에 대한 메타 데이터를 포함한 객체이다.

transform()메소드에서 return된 값은 Route 핸들러로 전해진다.
만약 예외(Exception)가 발생하면 클라이언트에 바로 전해진다.


mySQL 및 TypeORM

설치

  • typeorm
  • mysql
  • @nestjs/typeorm

이렇게 3가지 모듈을 설치한다.

1
npm install --save @nestjs/typeorm typeorm mysql

참고문서 documentation

설정

src 폴더 아래 configs 폴더 생성 후 그 안에 typeorm.config.ts 파일 생성

DAO단 분리

기존 내가 알던 model폴더에서 000.dao.ts 라고 파일명을 붙였던 것과 달리,
소스 강의에서는 000.repository.ts 파일명을 사용하고 있다. 그 외에는 동일하다.

게시물 삭제

remove()? vs delete()?

  • remove
    • 무조건 존재하는 아이템을 remove 메소드를 이용해서 지워야 한다. 그렇지 않으면 에러 발생(404 error)
  • delete
    • 만약 아이템이 존재하면 지우고 존재하지 않으면 아무런 영향이 없다.
    • 이러한 차이로 remove를 이용하면 하나의 아이템을 지울 때 두번 데이터베이스를 이용해야 하기 때문에 (아이템 유무 + 지우기) 소스강의에서는 데이터베이스에 한번만 접근해도 되는 delete 메소드를 사용한다.

Error Handling

에러를 잡아내고 싶다면 우선,

1
2
3
4
5
try {
	await this.save(user) // <- 이건 사용하고자 하는 코드
} catch(error) {
	console.log('error', error);
}

 

를 이용하여 에러를 확인할 수 있다.
http에서 해당 API를 돌려보면, 아래와 같은 로그를 터미널에서 확인할 수 있다.
 

1
2
3
4
5
6
7
 code: 'ER_DUP_ENTRY',
  errno: 1062,
  sqlMessage: "Duplicate entry 'test' for key 'user.IDX_78a916df40e02a9deb1c4b75ed'",
  sqlState: '23000',
  index: 0,
  sql: "INSERT INTO `user`(`id`, `username`, `password`) VALUES (DEFAULT, 'test', 'te171')"
}

 

해당 error code 확인 후,
다시 아래와 같은 방식으로 수정하면 정상적으로 잡고자 하는 에러를 잡아낼 수 있다.
 

1
2
3
4
5
6
7
8
9
try {  
  await this.save(user);  
} catch (error) {  
  if (error.code === 'ER_DUP_ENTRY') {  
    throw new ConflictException('Existing username');  
  } else {  
    throw new InternalServerErrorException();  
  }  
}

 

코드 수정 후, http Request 실행시
 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
HTTP/1.1 409 Conflict
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 67
ETag: W/"43-bvU5hJ7vN5ptmGJpSCIhkVNclIU"
Date: Mon, 16 Jan 2023 06:19:56 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{
  "statusCode": 409,
  "message": "Existing username",
  "error": "Conflict"
}

 

statusCode 409부터 정상적으로 송출하고자 하는 에러 메세지까지 모두 이상없음을 확인가능하다.


인증 및 인가 - bycriptjs

hashedPassword 생성 및 검증

우선 인증 인가 부분에 필요한 bcrypt를 설치한다.

1
npm install bcryptjs --save

 

import는 다음과 같다.

1
import * as bcrypt from 'bcryptjs';

 

회원가입시, bcrypt로 hashedPassword 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//user.repository.ts

@CustomRepository(User)  
export class UserRepository extends Repository<User> {  
  async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> {  
    const { username, password } = authCredentialsDto;  
  
    const salt = await bcrypt.genSalt();  // bcrypt가 들어간 코드
    const hashedPassword = await bcrypt.hash(password, salt); // bcrypt가 들어간 코드 
  
    const user = this.create({ username, password: hashedPassword });  // bcrypt가 들어간 코드
  
    try {  
      await this.save(user);  
    } catch (error) {  
      if (error.code === 'ER_DUP_ENTRY') {  
        throw new ConflictException('Existing username');  
      } else {  
        throw new InternalServerErrorException();  
      }  
    }  
  }  
}

 

로그인시, hashedPassword를 비교하여 검증 ( bcrypt.compare)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// user.service.ts

async signIn(authCredentialsDto: AuthCredentialsDto): Promise<string> {  
  const { username, password } = authCredentialsDto;  
  const user = await this.userRepository.findOneBy({ username });  
  
  if (user && (await bcrypt.compare(password, user.password))) {  
    return 'login success';  
  } else {  
    throw new UnauthorizedException('login failed');  
  }  
}

 

jwt token 생성

필요 모듈

  • @nestjs/jwt : nestjs에서 jwt를 사용하기 위해 필요한 모듈
  • @nestjs/passport : JWT를 이용하여 인증 처리하는 등의 과정을 훨씬 쉽게 만들어주는 모듈
1
npm install @nestjs/jwt @nestjs/passport passport passport-jwt --save

 

위 터미널 명령어와 같이 총 4개의 모듈을 설치한다.

  • @nestjs/jwt
  • @nestjs/passport
  • passport
  • passport-jwt  

passport 사용

 

Passport 모듈이란?
토큰이 유효한 토큰인지 서버에서 secret text를 이용해서 알아내면 payload 안에 유저 이름을 이용해서,
데이터베이스 안에 있는 유저 이름에 해당하는 유저 정보를 모두 가져올 수 있다.
이러한 처리를 쉽게 해주는게 Passport 모듈이다.

 

–> 쉽게 말해 로그인 이후, 토큰 확인 과정을 말한다!!!  

1
npm install @types/passport-jwt --save

 

Note
nestJS의 미들웨어
  • Pipes : 파이프는 요청 유효성 검사 및 페이로드 변환을 위해 만들어진다. 데이터를 예상한 대로 직렬화 한다.
  • Filters : 필터는 오류 처리 미들웨어이다. 특정 오류 처리기를 사용할 경로와 각 경로 주변의 복잡성을 관리하는 방법을 알 수 있다.
  • Guards : 가드는 인증 미들웨어. 지정된 경로로 통과할 수 있는 사람과 허용되지 않는 사람을 서버에 알려준다.
  • Interceptors : 인터셉터는 응답 매핑 및 캐시 관리와 함께 요청 로깅과 같은 전후 미들웨어. 각 요청 전후에 이를 실행하는 기능은 매우 강력하고 유용하다.

 

Tip

각각의 미들웨어가 불러지는(called) 순서

1
middleware -> guard -> interceptor (before) -> pipe -> controller -> service -> controller -> interceptor (after) -> filter (if applicable) -> client

 

UseGuards

UseGuards 안에 @nestjs/passport에서 가져온 AuthGuard()를 이용하면 요청안에 유저 정보를 넣어준 수 있다.

1
2
3
4
5
@Post('/authTest')
@UseGuards(AuthGuard())
authTest(@Req() req) {
	console.log(req)
}

 

UseGuards가 아닌 바로 객체에 접근하려면?

커스텀 데코레이터를 생성하여 접근할 수 있다.

1
2
3
4
5
6
7
import { createParamDecorator, ExcutionContext } from '@nestjs/common'
import { User } from './user.entity'

export const GetUser = createParamDecorator((data, ctx: ExcutionContext): User => {
	const req = ctx.switchToHttp().getRequest()
	return req.user
})

위 예시는, UseGuards를 사용하고 그 안에 있는 정보를 사용하는 방법이기에 UseGuards가 필요하다.


인증된 유저만 게시물을 사용할 수 있게 하기

우선 auth 모듈의 인증인가 부분을 boards 모듈에서도 사용할 수 있게 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// boards.module.ts 
@Module({  
  imports: [
	  TypeOrmExModule.forCustomRepository([BoardRepository]), 
	  AuthModule  // <- 추가
	  ],  
  controllers: [BoardsController],  
  providers: [BoardsService],  
})

//boards.controller.ts
@Controller('boards')  
@UseGuards(AuthGuard())  // <- 추가

컨트롤러 boards 아래 넣게되면 미들웨어처럼 작동하여,
모든 boards API는 토큰을 필요로 하게 된다.


유저와 게시물 데이터의 관계 형성

사용자 1명은 여러개의 게시물을 작성할 수 있다.
때문에 OneToMany Relationship, ManyToOne Relationship의 성격을 동시에 가지는데,
User Entity, Board Entity에서 각각 설정할 수 있다.
 

1
2
3
4
5
6
7
8
//user.entity.ts
@OneToMany(type => Board, board => board.user, { eager: true })
    boards: Board[]


//board.entity.ts
@ManyToOne((type) => User, (user) => user.boards, { eager: false })  
user: User;

 

Tip

option: eager

  • true 일때는 user정보를 가져올 때, board도 같이 가져옴

Log

로그의 종류

  • Log - 중요한 정보의 범용 로깅
  • Warning - 치명적이거나 파괴적이지 않은 처리되지 않은 문제
  • Error - 치명적이거나 파괴적인 처리되지 않은 문제
  • Debug - 개발자용 / 오류 발생시 로직을 디버그하는 데 도움이되는 유용한 정보입니다.
  • Verbose - 응용자용 / 응용 프로그램의 동작에 대한 통찰력을 제공하는 정보입니다.
예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// main.ts
import { NestFactory } from '@nestjs/core';  // <- this
import { AppModule } from './app.module';  
import { Logger } from '@nestjs/common';  
  
async function bootstrap() {  
  const app = await NestFactory.create(AppModule);  
  const port = 3000;  
  await app.listen(port);  
  Logger.log(`Application running on port ${port}`);  // <- this
}  
bootstrap();
 

 

1
2
3
4
5
6
7
// board.controller.ts

@Get()  
getAllBoard(@GetUser() user: User): Promise<Board[]> {  
  this.logger.verbose(`User ${user.username} trying to get all boards`); 
  return this.boardsService.getAllBoards(user);  
}

설정

.env 파일 사용 관련

필요 모듈 설치

1
2
3
4
npm install config --save

// 또는 
npm install dotenv --save

Related Content

0%