Nestjs 맛보기

배경

그전 까지는 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. 설치해야할 목록

  1. Node.js

  2. NestJs CLI

    sudo npm i -g @nestjs/cli
    nest --version
    

2.2. cli로 프로젝트 설치

nest new [project-name]

2.3. 시작하기전 Sample 제거 세팅



3. nestjs 구조와 개념

3.1. 기본 Nestjs req/res 설명

Nestjs 아키텍쳐
Nestjs 아키텍쳐

  1. Client가 Request 요청
  2. Request URL에 맞는 Controller가 수신
  3. Controller는 해당 요청을 처리하기 위한 Service 호출
  4. Service는 알맞은 정보를 가공 및 처리하여 Controller에게 전달
  5. ControllerService의 결과를 response

3.2. Module에 대해

Module
Module

  1. 기본적으로 root가 되는 App Module로 진입한다.

  2. 모듈은 관련된 기능 집합으로 구성하는 것이 효과적이다.

    • User Module : 사용자 관련
    • Board Module : 게시판 관련
    • Product Module : 상품 관련
  3. 모듈은 기본적으로 싱글통이므로 여러 모듈간에 쉽게 공급자의 동일한 인스턴스를 공유 할 수 있다.

    → User Module, Board Module에서 둘다 사용하는 모듈의 경우 Common Module와 같이 공통 모듈로 사용할 수 잇다.


3.3. Controller에 대해

컨트롤러는 들어오는 요청을 처리하고 클라이언트에 응답을 반환한다.

Controller
Controller

클라이언트의 요청이 Board에 관한 것이라면 Board Controller에 요청을 받고 반환한다.

3.3.1. Controller의 구조

@Controller로 데코레이터로 정의를 하여 사용한다.

데코레이터에 있는 파라미터가 해당 컨트롤러의 Path가 된다.

Controller 구조
Controller 구조

다음 컨트롤러에 @Controller(’user’)를 사용하면 user컨트롤러가 된다.


3.4. Service

주로 DB 관련된 로직을 처리한다.

(DB에서 데이터를 가지고 오거나, 값을 추가하는 로직 처리 등)


3.5. Model

데이터의 타입 정의한다고 보면 된다.

DB를 연동할 경우 Entity를 사용하므로 model 파일은 필요가 없어진다.

코드 제너레이터 명령어가 따로 있지 않으므로 [모델명].model.ts로 만든다.



4. Nestjs 코드 제너레이터 명령어

4.1. Module 생성

nest g moudule [모듈명]

4.2. Controller 생성

nest g controller [컨트롤러명] --no-spec

--no-spec : 해당 옵션은 spec파일을 생성하지 않게하는 것이다.


4.3. Service 생성

nest g service [컨트롤러명] --no-spec

--no-spec : 해당 옵션은 spec파일을 생성하지 않게하는 것이다.



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;
  }
}
  1. private로 변수를 만드는 이유는 다른 곳에서 해당 변수를 사용하지 않도록 하기 위함이다.

  2. 모든 게시물의 데이터를 가지고 오는 getAllBoards 메서드 생성

    해당 메서드를 Controller

5.1.2. GET요청 Controller 만들기

@Controller("boards")
export class BoardsController {
  constructor(private boardsService: BoardsService) {}

  @Get()
  getAllBoard() {
    return this.boardsService.getAllBoards();
  }
}
  1. constructor에서 사용할 service를 연결한다.
  2. 해당 서비스인 boardsServicegetAllBoard()를 사용하는 것은 다음과 같다.
    • @Get() 은 요청 메서드 종류
    • getAllBoard() {} 는 service에 있는 메서드명
    • return this.boardsService.getAllBoards()는 boardsService의 getAllBoards를 뜻한다.

5.1.3. Get query 요청하기

예제로 id값을 query로 get요청을 받아서 board에서 id값이 일치하는 값을 반환하는 API 생성할때로 가정한다.


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 만들기

이렇게 서비스와 컨트롤러를 구성했을 경우 paramter에 대한 수정 사항이 생겼다고 가졍한다면 귀찮은 일이 샌긴다.

만약 description을 안받는다고 했을 경우 3부분을 수정해야한다. (이 수정되는 파라미터가 열개가 넘는다면?)

이럴 경우 DTO를 통해 해결 할 수 있다.

DTO에 대한 설명은 6. DTO (Data Transfer Object) 에서 자세히 볼 수 있다.

5.2.3. DTO 생성 및 적용

  1. 다음과 같은 폴더 구조로 dto를 생성한다.

dto
dto

  1. 그리고 위에서 사용할 파라미터를 다음과 같이 만든다.

    export class CreateBoardDto {
      title: string;
      description: string;
    }
    
  2. DTO 적용

    • Controller 바뀌기전

    Controller previous
    Controller previous

    • Controller 바뀌기후

    Controller after
    Controller after

    • Service 바뀌기전

    Service previous
    Service previous

    • Service 바뀌기후

    Service after
    Service after


5.3. Delete


5.4. Update

업데이트는 PUT와 PATCH 두개가 있다.

5.4.1. PATCH

예제는 id값의 status를 업데이트할 때이다.



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
Pipe

  1. 요청해서 오는 인수들을 Pipe에서 유효성 체크를 한다.
  2. 성공시 Controller에 의 의해
  3. 만약 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가지로 나누어 진다.

7.2.1. Handler-level Pipes

핸들러 래밸에서 @UsePipes() 데코레이터를 이용해서 사용할 숫 있다.

이 파이프는 모든 파라미터에 적용된다.

Pipe
Pipe

7.2.2. Parameter-level Pipes

4.1. 보다 좁은 범위의 파라미터 레벨의 파이프며, 특정한 파라미터에게만 적용이 되는 파이프이다.

Pipe
Pipe

title 하나에만 적용된다.

7.2.3. Global-level Pipes

모든 범위에서 적용되는 파이프이다.

클라이언트에서 들어오는 모든 요청에 적용 된다.

Pipe
Pipe


7.3. Built-in Pipes

기본적으로 nest에서는 6가지 파이프를 지원한다.


7.4. Pipe 사용하기

yarn add class-validator
yarn add class-transformer

1번 부분을 보면 Pipe는 두가지를 담당한다고 했다.

그 두 담당 부분은 위의 명령어로 각각 설치할 수 있다.

7.4.1. 사용 예시

사용 해보기 1

  1. dto에 유효성 검사 데코레이터 추가

    @IsNotEmpty()는 값이 비었을때 체크를 한다.

    Pipe example1
    Pipe example1

  2. Pipe를 어떤 범위에 등록할 건지를 등록

    2. 에 있던 범위 참고

    Pipe example1
    Pipe example1

    여기에서 ValidationPipe는 3.3. 의 nestjs의 Built-in Pipe이다.

  3. 실행 결과

    값이 없이 Post할 경우 다음과같이 반환 해준다.

    Pipe example1
    Pipe example1

사용 해보기 2

특정 값을 조회할때 값이 없을 경우 아무값도 리턴하지 않는다.

그럴때 예외 처리하는 방법이 있다.

  1. 특정 id를 조회하는 service 부분에서 다음과 같이 고쳐준다.

    Pipe example2
    Pipe example2

  2. NotFoundException()은 nest에서 기본적으로 지원한다.

    Pipe example2
    Pipe example2

  3. 다음과 같이 결과를 반환한다.

    Pipe example2
    Pipe example2

  4. 만약 원하는 값으로 반환 할 경우 다음과 같이 문구만 넣어 주면 된다.

    Pipe example2
    Pipe example2

    Pipe example2
    Pipe example2

사용 해보기 3

특정 ID로 가져올때 없는 아이디의 게시물을 가져오려고 하면 그에 대한 에러 값을 전달해주었던 것처럼 없는 게시물을 지우려 할때에도 에러를 줄 수 있다.

4.1.2. 사용해보기2 에서 했던 게시물이 있는지 체크를 해준 후에 있을 경우만 지워주고, 아닐 경우 에러 문구를 반환하면 된다.

Pipe example3
Pipe example3


7.5. 커스텀 파이프

7.5.1. 사용해보기

다음과 같이 구성한다.

Custom Pipe example
Custom Pipe example

  1. 커스텀 파이프 생성

    Custom Pipe example
    Custom Pipe example

    기본 틀

    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;
      }
    }
    
  2. 적용

    status를 수 느하서 에p 에서다 과 같이 적용할 수 있다.

    param중 status 개별로만 하는 것이기때문에 다음과 같이 사용한다.

    Custom Pipe example
    Custom Pipe example



8. TypeOrm

8.1. TypeORM 이란?

nodejs에서 실행되고 TS로 작성된 객체 관계형 메퍼 라이브러리이다.

8.1.1. ORM 이란?

Object Realational Mapping

객체와 관계형 DB데이터를 자동으로 변형 및 연결하는 작업

ORM을 이용한 개발은 객체와 데이터베이스의 변형에 유연하게 사용할 수 있다.

typeOrm
typeOrm


8.2. nestjs에서 TypeORM 사용하기

  1. @nestjs/typeorm

    : nestJS에서 TypeORM을 사용하기 위해 연동하는 모듈

  2. typeorm

    : typeORM 모듈

  3. pg

    : Postgres 모듈

8.2.1. nestjs TypeORM 연동

  1. 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,
    };
    
  2. 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;
}

→ 데코레이터 설명


8.4. Repository

typeOrm
typeOrm

DB에 관련된 일은 서비스에서하는 것이 아닌 Repository에서 해준다.

이것을 Repositorty Pattern이라고도 부른다.

  1. 레포지토리 파일 생성
  2. 생성한 파일에 리포지토리르 위한 클래스 생성
    • 생성시 Repository 클래스를 Extends 해준다. (Find, Insert, Delete 등 엔티티를 컨트롤 해줄 수 있습니다.)
    • @EntityRepository() : 클래스를 사용자 정의(커스텀) 저장소로 선언하는데 사용된다.
  3. 생성한 Repository를 다른 곳에서도 사용할 수 있기 위해서 board.moudle에서 import해준다.

@EntityRepository() → 해당 부분은 현재 버전에서 사용 되지 않는다. "typeorm": "^0.2.41" 으로 변경해서 사용했다.