Nestjs 맛보기
2022.12.27
배경
그전 까지는 BackEnd가 필요할때 단순한 Express나 Next만 사용해서 Route, Controller 정도만 구분하여 사용하는데 그쳤다면 어느정도 규모가 있는 프로젝트를 하다 보니 코드를 더이상 관리하기가 힘들어 졌.
그래서 규모가 있는 프로젝트를 접하고 로직이 서로 엉키고, 복잡하다 보니 데이터 처리에 대한 아키텍쳐 패턴에 고민을 하게 되었고 해당 아키텍쳐에 대해 학습하기 위해 Nestjs를 선택했다.
Nestjs는 code generator로 코드를 구분해서 코드를 Repository, Controller, Service, Module로 알아서 나누어서 코드 컨벤션이 정해져 있기에 백엔드 아키텍쳐에 대해 공부하기 좋다고 생각을 햇다.
1. Nestjs에 대해
Nestjs는 Node.js 서버측 애플리케이션 프레임워크이다.
TypeScript, OOP(Object Oriented Programming), FP(Functional Programming), FRP(Functional Reactive Programming) 요소를 사용할 수 있게한다.
내부적으로는 Express/ Fastify 중에서 선택적으로 사용할 수 있게 구성할 수 있다.
Nestjs는 개발자와 팀이 고도로 테스트 가능하고 확장 가능하며, 느슨하게 결합되고, 유지 관리가 쉬운 애플리케이션을 만들 수 있는 즉시 사용 가능한 애플리케이션 아키텍처를 제공한다.
(→ 기존에 Node에서 가장 많이 사용하는 Express에 비해 여러 기능이 명령어로 실행 시킬 수 있다.)
2. Nestjs 기본 설치와 세팅
2.1. 설치해야할 목록
-
Node.js
-
NestJs CLI
sudo npm i -g @nestjs/cli nest --version
2.2. cli로 프로젝트 설치
nest new [project-name]
2.3. 시작하기전 Sample 제거 세팅
-
기본 파일 제거
-
test폴더 및 main.ts와 app.module.ts 빼고 제거
-
app.module.ts에 제거한 파일 부분 코드 수정
-
3. nestjs 구조와 개념
3.1. 기본 Nestjs req/res 설명
- Client가 Request 요청
- Request URL에 맞는 Controller가 수신
Controller
는 해당 요청을 처리하기 위한Service
호출Service
는 알맞은 정보를 가공 및 처리하여Controller
에게 전달Controller
는Service
의 결과를 response
3.2. Module에 대해
-
기본적으로 root가 되는 App Module로 진입한다.
-
모듈은 관련된 기능 집합으로 구성하는 것이 효과적이다.
- User Module : 사용자 관련
- Board Module : 게시판 관련
- Product Module : 상품 관련
-
모듈은 기본적으로 싱글통이므로 여러 모듈간에 쉽게 공급자의 동일한 인스턴스를 공유 할 수 있다.
→ User Module, Board Module에서 둘다 사용하는 모듈의 경우
Common Module
와 같이 공통 모듈로 사용할 수 잇다.
3.3. Controller에 대해
컨트롤러는 들어오는 요청을 처리하고 클라이언트에 응답을 반환한다.
클라이언트의 요청이 Board에 관한 것이라면 Board Controller에 요청을 받고 반환한다.
3.3.1. Controller의 구조
@Controller
로 데코레이터로 정의를 하여 사용한다.
데코레이터에 있는 파라미터가 해당 컨트롤러의 Path가 된다.
다음 컨트롤러에 @Controller(’user’)를 사용하면 user컨트롤러가 된다.
- Handler란 @Get, @Post, @Delete 등과 같은 데코레이터는 컨트롤러 클래스 내의 단순한 메서드이다.
3.4. Service
주로 DB 관련된 로직을 처리한다.
(DB에서 데이터를 가지고 오거나, 값을 추가하는 로직 처리 등)
-
Provider란?
프로바이더는 종속성으로 주입할 수 있다.
Controller에서 여러 Service가 있을때 @Injectable()를 사용하여 할 수 있다.
-
Provider 등록하기
module 파일에 providers 항목안에 해당 모듈을 사용하고자 하는 Provider를 넣어서 할 수 있다.
-
Service 종속성 추가하기
Service를 Controller에서 이용할 수 있게 해주기 위한 Dependency Injection을 해야한다.
3.5. Model
데이터의 타입 정의한다고 보면 된다.
DB를 연동할 경우 Entity를 사용하므로 model 파일은 필요가 없어진다.
코드 제너레이터 명령어가 따로 있지 않으므로 [모델명].model.ts
로 만든다.
-
모델 정의 방식
-
interface
변수의 타입만을 체크한다.
-
class
변수의 타입도 체크하고 인스턴스 또한 생성할 수가 있다.
-
4. Nestjs 코드 제너레이터 명령어
4.1. Module 생성
nest g moudule [모듈명]
4.2. Controller 생성
nest g controller [컨트롤러명] --no-spec
--no-spec : 해당 옵션은 spec파일을 생성하지 않게하는 것이다.
-
Controller 생성 순서
-
컨트롤러명과 같은 폴더 찾기
-
해당 폴더 안에 controller 파일 생성
-
해당 폴더안에 module 파일 찾기
-
module 파일 안에 controller 추가하기
→ 즉, module에 따로 추가 하지 않아도 된다.
-
4.3. Service 생성
nest g service [컨트롤러명] --no-spec
--no-spec : 해당 옵션은 spec파일을 생성하지 않게하는 것이다.
-
추가 후 설명
-
controller 생성과 마찬가지로 module에 자동으로 추가 된다.
-
하지만 Service를 Controller에서 이용할 수 있게 해주기 위한 Dependency Injection을 해야한다.
NestJS에서 Dependency Injection는 클래스의 Constructor 안에서만 이루어진다.
-
5. CRUD 생성하기
5.1. GET
5.1.1. GET요청 Service 만들기
import { Injectable } from "@nestjs/common";
@Injectable()
export class BoardsService {
private boards = [];
getAllBoards() {
return this.boards;
}
}
-
private
로 변수를 만드는 이유는 다른 곳에서 해당 변수를 사용하지 않도록 하기 위함이다. -
모든 게시물의 데이터를 가지고 오는
getAllBoards
메서드 생성해당 메서드를 Controller
5.1.2. GET요청 Controller 만들기
@Controller("boards")
export class BoardsController {
constructor(private boardsService: BoardsService) {}
@Get()
getAllBoard() {
return this.boardsService.getAllBoards();
}
}
- constructor에서 사용할 service를 연결한다.
- 해당 서비스인
boardsService
의getAllBoard()
를 사용하는 것은 다음과 같다.@Get()
은 요청 메서드 종류getAllBoard() {}
는 service에 있는 메서드명return this.boardsService.getAllBoards()
는 boardsService의 getAllBoards를 뜻한다.
5.1.3. Get query 요청하기
예제로 id값을 query로 get요청을 받아서 board에서 id값이 일치하는 값을 반환하는 API 생성할때로 가정한다.
-
Service
-
Controller 위와 같이
:id
로 받을 파라미터를@GET
데코레이터 입력하고 해당 값을 넘겨 받을 때는@Param(’id’) id
로 받는다.-
하나의 param을 받을때
-
여러개의 param을 받을때
-
5.2. POST
5.2.1. Post요청 Service 만들기
createBoard(title: string, description: string) {
const board: Board = {
id: uuid(),
title,
description,
status: BoardStatus.PUBLIC,
};
this.boards.push(board);
return board;
}
id는 유니크한 값이므로 uuid패키지를 설치해서 사용한다.
5.2.2. Post요청 Controller 만들기
-
@Body() body
로 하면 req할때 보낸 데이터를 받을 수 있다. -
@Body(’title) title
,@Body(’desciption) desciption
으로 하면 해당 값만 받을 수 도 있다.
이렇게 서비스와 컨트롤러를 구성했을 경우 paramter에 대한 수정 사항이 생겼다고 가졍한다면 귀찮은 일이 샌긴다.
만약 description을 안받는다고 했을 경우 3부분을 수정해야한다. (이 수정되는 파라미터가 열개가 넘는다면?)
이럴 경우 DTO를 통해 해결 할 수 있다.
DTO에 대한 설명은 6. DTO (Data Transfer Object) 에서 자세히 볼 수 있다.
5.2.3. DTO 생성 및 적용
- 다음과 같은 폴더 구조로 dto를 생성한다.
-
그리고 위에서 사용할 파라미터를 다음과 같이 만든다.
export class CreateBoardDto { title: string; description: string; }
-
DTO 적용
- Controller 바뀌기전
- Controller 바뀌기후
- Service 바뀌기전
- Service 바뀌기후
5.3. Delete
-
Service 요청한 id와 같지 않은 값들은 지우는 코드
-
Controller
5.4. Update
업데이트는 PUT와 PATCH 두개가 있다.
- PUT : 모든 리소스를 업데이트
- PATCH : 일부 리소스만 업데이트
5.4.1. PATCH
예제는 id값의 status를 업데이트할 때이다.
-
Service
-
Controller
6. DTO (Data Transfer Object)
DTO는 계층간에 데이터 교환을 위한 객체이다.
DB에서 데이터를 얻어서 Service나 Controller등으로 보낼 떄 사용하는 객체를 뜻한다.
데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체이다.
interface나 class를 이용해서 정의할 수 있지만, Nestjs에서는 클래스를 이용하는 것을 추천한다.
class는 interface와 다르게 런타임에서 작동하기 때문에 pipe 기능을 사용할때 유용하다.
- 데이터 유효성을 체크하는데 효율적
- 안정적인 코드로 만들어줌
7. Pipe
7.1. Pipe란?
파이프는 @Injectable() 데코레이터로 주석이 달린 클래스이다.
data transformation과 data validation을 위해서 사용된다.
컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.
- 요청해서 오는 인수들을 Pipe에서 유효성 체크를 한다.
- 성공시 Controller에 의 의해
- 만약 Pipe에서 실패면 Errorr가 된다.
만약 Pipe가 없으면 바로 @Get Route ~부분으로 넘어가게 된다.
7.1.1. Data Transformation 이란?
데이터 형식을 변환 하는 것을 뜻한다.
예를 들면 숫자 100
을 문자열 ‘100’
으로 바꾼다.
(Number → String)
7.1.2. Data validation 이란?
유효성 체크
입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달하면 된다.
올바르지 않을 경우 예외를 발생 시킨다.
7.2. Pipe 사용 방법(Binding Pipes)
파이프를 사용하는 방법은 다음 3가지로 나누어 진다.
- Handler-level Pipes
- Parameter-level Pipes
- Global-level Pipes
7.2.1. Handler-level Pipes
핸들러 래밸에서 @UsePipes() 데코레이터를 이용해서 사용할 숫 있다.
이 파이프는 모든 파라미터에 적용된다.
7.2.2. Parameter-level Pipes
4.1. 보다 좁은 범위의 파라미터 레벨의 파이프며, 특정한 파라미터에게만 적용이 되는 파이프이다.
title 하나에만 적용된다.
7.2.3. Global-level Pipes
모든 범위에서 적용되는 파이프이다.
클라이언트에서 들어오는 모든 요청에 적용 된다.
7.3. Built-in Pipes
기본적으로 nest에서는 6가지 파이프를 지원한다.
- validationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
7.4. Pipe 사용하기
yarn add class-validator
yarn add class-transformer
1번 부분을 보면 Pipe는 두가지를 담당한다고 했다.
그 두 담당 부분은 위의 명령어로 각각 설치할 수 있다.
-
사용 예시와 데코레이더 목록
GitHub - typestack/class-validator: Decorator-based property validation for classes.
참고 설명
여기에서@IsNotEmpty()
를 사용하여 값이 없을 경우 예외 처리를 할 수 있다.
7.4.1. 사용 예시
사용 해보기 1
-
dto에 유효성 검사 데코레이터 추가
@IsNotEmpty()
는 값이 비었을때 체크를 한다. -
Pipe를 어떤 범위에 등록할 건지를 등록
2. 에 있던 범위 참고
여기에서 ValidationPipe는 3.3. 의 nestjs의 Built-in Pipe이다.
-
실행 결과
값이 없이 Post할 경우 다음과같이 반환 해준다.
사용 해보기 2
특정 값을 조회할때 값이 없을 경우 아무값도 리턴하지 않는다.
그럴때 예외 처리하는 방법이 있다.
-
특정 id를 조회하는 service 부분에서 다음과 같이 고쳐준다.
-
NotFoundException()은 nest에서 기본적으로 지원한다.
-
다음과 같이 결과를 반환한다.
-
만약 원하는 값으로 반환 할 경우 다음과 같이 문구만 넣어 주면 된다.
사용 해보기 3
특정 ID로 가져올때 없는 아이디의 게시물을 가져오려고 하면 그에 대한 에러 값을 전달해주었던 것처럼 없는 게시물을 지우려 할때에도 에러를 줄 수 있다.
4.1.2. 사용해보기2 에서 했던 게시물이 있는지 체크를 해준 후에 있을 경우만 지워주고, 아닐 경우 에러 문구를 반환하면 된다.
7.5. 커스텀 파이프
- transform() 메서드
이 메서드는 value, metadata 파라미터를 가진다.
- value는 처리가 된 인자의 값
- metadata는 인자에 대한 메타 데이터를 포함한 객체 return된 값은 Route 핸들러로 전해져서 예외(Exception)이 발생하면 클라이언트에 바로 전달 된다.
7.5.1. 사용해보기
다음과 같이 구성한다.
-
커스텀 파이프 생성
기본 틀
export class BoardStatusValidationPipe implements PipeTransform { readonly StatusOptions = [BoardStatus.PRIVATE, BoardStatus.PUBLIC]; transform(value: any, metadata: ArgumentMetadata) { value = value.toUpperCase(); // status가 유효하지 않을 경우 예외 처리 if (!this.isStatusValid(value)) { throw new BadRequestException(`${value} isn't in the status options`); } return value; } /** * status가 유효한지 체크 * @param status * @returns 유효한 경우 반환 */ private isStatusValid(status: any) { const index = this.StatusOptions.indexOf(status); return index !== -1; } }
-
적용
status를 수 느하서 에p 에서다 과 같이 적용할 수 있다.
param중 status 개별로만 하는 것이기때문에 다음과 같이 사용한다.
8. TypeOrm
8.1. TypeORM 이란?
nodejs에서 실행되고 TS로 작성된 객체 관계형 메퍼 라이브러리이다.
8.1.1. ORM 이란?
Object Realational Mapping
객체와 관계형 DB데이터를 자동으로 변형 및 연결하는 작업
ORM을 이용한 개발은 객체와 데이터베이스의 변형에 유연하게 사용할 수 있다.
8.2. nestjs에서 TypeORM 사용하기
- 설치
yarn add pg typeorm @nestjs/typeorm
-
@nestjs/typeorm
: nestJS에서 TypeORM을 사용하기 위해 연동하는 모듈
-
typeorm
: typeORM 모듈
-
pg
: Postgres 모듈
8.2.1. nestjs TypeORM 연동
-
configs/typeorm.config.ts
해당 경로의 파일이름으로 typeorm연결하는 코드를 작성
import { TypeOrmModuleOptions } from "@nestjs/typeorm"; export const typeORMConfig: TypeOrmModuleOptions = { type: "postgres", host: "localhost", port: 5432, username: "postgres", password: "postgres", database: "boardapp", entities: [__dirname + "/../**/*.entity.{js,ts}"], synchronize: true, };
-
TypeORM을 root 모듈에 추가
app.module.ts
에 추가import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { BoardsModule } from "./boards/boards.module"; import { typeORMConfig } from "./configs/typeorm.config"; @Module({ imports: [TypeOrmModule.forRoot(typeORMConfig), BoardsModule], }) export class AppModule {}
8.3. Entity
TypeORM을 사용할때는 DB 테이블로변환되는 Class이기 때문에 클래스를 생성하고 그 안에 컬럼을 정의해준다.
@Entity()
export class Board extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
@Column()
status: BoardStatus;
}
→ 데코레이터 설명
- @Entity() : Board 클래스가 엔티티임을 나타내는데 사용된다.
- @PrimaryGeneratedColumn() : Board 엔티티의 기본 키 열임을 나타내는데 사용
- @Column() Board 엔티티의 title 및 description과 같은 다른 열을 나타내는데 사용
8.4. Repository
DB에 관련된 일은 서비스에서하는 것이 아닌 Repository에서 해준다.
이것을 Repositorty Pattern이라고도 부른다.
- 레포지토리 파일 생성
- 생성한 파일에 리포지토리르 위한 클래스 생성
- 생성시 Repository 클래스를 Extends 해준다. (Find, Insert, Delete 등 엔티티를 컨트롤 해줄 수 있습니다.)
- @EntityRepository() : 클래스를 사용자 정의(커스텀) 저장소로 선언하는데 사용된다.
- 생성한 Repository를 다른 곳에서도 사용할 수 있기 위해서 board.moudle에서 import해준다.
@EntityRepository() → 해당 부분은 현재 버전에서 사용 되지 않는다.
"typeorm": "^0.2.41"
으로 변경해서 사용했다.