NestJS 정리 1편

GGAE99·2023년 5월 18일
0
post-thumbnail

NestJS가 뭐야?

NestJS는 자바스크립트나 타입스크립트로 서버 애플리케이션을 개발할 수 있는 백앤드 웹 프레임워크다.
자바의 스프링(Spring), 파이썬의 장고(Django)등이 백엔드 웹 프레임워크이다.

타입 스크립트를 사용하여 엄격하게 제한하는 것 또한 Nest의 장점이다.
기본적으로 Express를 사용하지만, 선택적으로 Fastify를 사용하도록 구성 할 수도 있다!

NestJS를 사용하기 위해서는 기본적으로 Nodejs를 설치해놓은 상태여야한다.

NestJS 공식 문서

NestJS 프로젝트 시작

NestJS 설치

$ npm i -g @nestjs/cli => nest cli 도구를 전역으로 설치
$ nest new 생성 프로젝트 이름 => nest project 생성
? Which package manager would you ❤️  to use? (Use arrow keys) => 패키지 매니저 선택
❯ npm
  yarn
  pnpm  

NestJS CLI

nest

nest 명령어로 NestJS의 다양한 CLI를 확인할 수 있다.
nest명령어

example

nest g controller boards --no-spec
=> boards 라는 폴더에 boards.controller를 생성 / spec(테스트를 위한 소스 코드)를 생성하지 않을 것

NestJS 프로젝트 요소

package.json

현재 프로젝트에 관한 정보와 패키지 매니저(npm, yarn 등)을 통해 설치한 모듈들의 의존성을 관리하는 파일이다.

eslintrc.js

개발자들이 특정한 규칙을 가지고 코드를 깔끔하게 짤수있게 도와주는 라이브러리
타입스크립트를 쓰는 가이드 라인 제시, 문법에 오류가 나면 알려주는 역할 등등

prettierrc

주로 코드 형식을 맞추는데 사용한다.
작은따옴표(')를 사용할지 큰 따옴표(")를 사용할지, Indent 값을 2로 줄지 4로 줄지등등,
에러 찾는것이 아닌 코드 포맷터 역할.

nest-cli.json

nest 프로젝트를 위해 특정한 설정을 할 수 있는 json 파일

tsconfig.json

어떻게 타입스크립트를 컴파일 할지 설정

tsconfig.build.json

tsconfig.json의 연장선상 파일이며 , build를 할 때 필요한 설정들
"excludes"에서는 빌드할 때 필요 없는 파일들 명시

src 폴더

대부분의 비즈니스 로직이 들어가는 폴더

데코레이터

데코레이터는 함수를 명시적으로 수정하지 않고도 확장하거나 기능확장을 시킬수 있는 방법이다.
@(데코레이터 이름) 의 방법을 사용해 명시한다.

NestJS 흐름 (module, cotroller, service)

Nest를 공부하시는 분들은 express를 잘 아실거라고 생각한다.
express와 간단하게 비교해서 설명하자면

server.js = app.module.ts => 진입점, 진입url
(some).routes.js = (some).controller.ts => Get,Post,Delete,Path 등을 구분 및 요청 시 실행
controller/(some).js = (some)service.ts => db 로직 등 작성

main.ts

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

http://localhost:3000에 접속할 수 있도록 도와주는 파일
진입점인 AppModule을 가져온다.

app.module.ts (~.module.ts)

module 이란?
모듈은 밀접하게 관련된 기능 집합으로 구성 요소를 구성하는 효과적인 방법이다.
app.module.ts는 모듈이지만 기반인 되는 모듈이라서 imports에 다른 모듈들을 넣어주는 역할을 한다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

//@Module = 모듈 위에 붙는 데코레이터
@Module({
  imports: [], //여기에 다른 모듈들을 넣어줌
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app 외의 다른 모듈들은 imports 안에 넣어준다.
boards 모듈을 생성한다고 했을 때, app 모듈이 app 컨트롤러와 app 서비스를 포함하고 있는 것 처럼
boards 컨트롤러, boards 서비스 등을 포함하도록 생성하고 imports는 boards 모듈만 넣어준다.

app.controller.ts (~.controller.ts)

Controller 란 ?
컨트롤러는 들어오는 요청을 처리하고 클라이언트에 응답을 반환한다. 라우터의 역할을 한다.
NestJS는 컨트롤러를 비즈니스 로직이랑 구분 짓고 싶어한다.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

//@Controller() = Controller 위에 붙는 데코레이터
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Controller 데코레이터는 인자를 Controller에 의해서 처리되는 "경로"로 받는다.

@Controller('boards')

@Get('/') // 사실 받는 주소는 "http://localhost:3000/boards"의 Get요청이다.
    getAllBoards(): Board[] {
        return this.boardService.getAllBoards();
    }

@Get, @Post, @Delete, @Path는 Handler이다. 요청의 종류를 구분하는 메소스다.

app.service.ts (~.service.ts)

service 란?
@Injectable 주입할 수 있다는 뜻 데코레이터로 감싸져서 모듈에 제공되며, 이 서비스 인스턴스는 애플리케이션 전체에서 사용 될 수 있다. 컨트롤러에서 데이터의 유효성 체크를 하거나 데이터베이스에 아이템을 생성하는 등의 작업을 하는 부분을 처리한다.
간단하게 비즈니스 로직
주로 db 작업등을 여기서 수행

import { Injectable } from '@nestjs/common';

//@Injectable() = Service 위에 붙는 데코레이터
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

@Injectable 데코레이터를 설정해주는 것은 Service를 Provider로 만든다는 뜻이다.

흐름 정리

서버에 접속
=> main.ts (서버 접속)
=> app.module.ts (다양한 모듈을 포함하는 app 모듈로 들어가 url에 따라 다른 라우터 연결)
=> ~.controller.ts (메소드 구별 및 Service 로직 가져옴)
=> ~.service.ts (비즈니스 로직 수행)

Model

필요한 데이터가 어떤것이 있는지를 정의해주기 위해서 게시물의 모델을 만들어주는 것이 좋다.
예를들어 게시물 데이터에는 ID가 필요하고 이름이나 설명등이 필요하다니 title, id 등의 요소가 필요하다.

#board.model.ts

export interface Board {
    id: string;
    title: string;
    description: string;
    status: BoardStatus;
}

export enum BoardStatus {
    PUBLIC = 'PUBLIC',
    PRIVATE = 'PRIVATE',
}

Board라는 모델을 정의하고, (id, title, description, status) 정의
status의 타입은 BoardStatus로 제한
enum(enumeration)은 타입스크립트의 기능이다.

Body

Express에서는 bodyParser 모듈을 이용해서
req.body 방식으로 클라이언트에서 보내온 값을 받아왔다.
NestJS에서는 @Body body를 사용해서 가져온다.

#boards.controller.ts의 Post 메소드 중 하나

@Post('/')
@UsePipes(ValidationPipe)
createBoard(@Body('title') title:string, @Body('description') description:string )  {
    return this.boardService.createBoard(title, description);
}

이렇게 보내주는 변수의 이름을 사용하여 받아오는 것이 가능하다.
하지만 보내주는 변수가 많을 때는 하나하나 가져오는게 힘들다.
그래서 DTO를 사용할 수 있다.
DTO는 바로 다음에 설명하겠다.

@Post('/')
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
   return this.boardService.createBoard(createBoardDto);
}

DTO

계층간 데이터 교환을 위한 객체다.
DTO는 데이터가 네트워크를 통해 전송되는 방법을 정의하는 객체다.
interface나 class를 이용해서 정의 될 수 있지만 NestJS에서는 클래스를 이용하는것을 추천하고 있다.

#create-board.dto.ts

import { IsNotEmpty } from "class-validator";

export class CreateBoardDto {
    @IsNotEmpty()
    title: string;

    @IsNotEmpty()
    description: string;
}

DTO (Data Transfer Object)를 쓰는 이유는 무엇일까?

  • 데이터 유효성을 체크하는데 효율적이다.

  • 더 안정적인 코드로 만들어 준다. 타입스크립트의 타입으로도 사용할 수 있다.

partialType

partialType은 DTO를 물려받는 것 이라고 생각하면 편하다.

#update-board.dto.ts

import { PartialType } from "@nestjs/mapped-types";
import { CreateBoardDto } from "./create-board.dto";

export class UpdateBoardDto extends PartialType(CreateBoardDto){}

이렇게 이전에 정의했던 dto 속성을 재사용할 수 있다.

Param

param을 이용하여 url내의 값을 가져올 수 있다.

#boards.controller.ts

// http://localhost:3000/boards/12345 => id = 12345
@Get('/:id')
getBoardId(@Param('id') id: string): Board {
    return this.boardService.getBoardId(id);
}

Pipe

Pipe가 뭐야?

Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드로 향하는 인수를 수신하고 이에 대해 작동한다.

  • @Injectable () 데코레이터로 주석이 달린 클래스다.
  • data transformation과 data validation을 위해서 사용된다.
  • 컨트롤러 경로 처리기에 의해 처리되는 인수에 대해 작동한다.

data transformation?

입력 데이터를 원하는 형식으로 변환하는 것 (예 : 문자열에서 정수로)
만약 숫자를 받길 원하는데 문자열 형식으로 온다면 파이프에서 자동으로 숫자로 바꿔줍니다.

data validation?

입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달하면됩니다.
그렇지 않으면 데이터가 올바르지 않을 때 예외를 발생시킵니다.
만약 이름의 길이가 10자 이하여야 하는데 10자 이상 되면 에러를 발생시킵니다.

pipe 사용 경우

라우트 핸들러(Route Handler)가 처리하는 인수에 대해서 작동한다.
그리고 파이프는 메소드를 바로 직전에 작동해서 메소드로 향하는 인수에 대해서 변환할 것이 있으면 변환하고 유효성 체크를 위해서도 호출된다.

pipe 사용법

Handler-level Pipes ,Parameter-level Pipes, Global-level Pipes 이 존재한다.
이름에서 말하는 것 그대로 핸들러 레벨, 파라미터 레벨, 글로벌 레벨로 파이프 사용할 수 있다.

class-validator , class-transformer 있어야 사용 가능
npm install class-validator class-transformer  --save

Handler-level Pipes
핸들러 레벨에서 @UsePipes() 데코레이터를 이용해서 사용 할 수 있다.
이 파이프는 모든 파라미터에 적용이 된다. (CreateBoardDto)

# example
@Post('/')
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardService.createBoard(createBoardDto);
}

Parameter-level Pipes
파라미터 레벨의 파이프 이기에 특정한 파라미터에게만 적용이 되는 파이프다.
아래와 같은 경우에는 title만 파라미터 파이프가 적용된다.

# example
@Post('/')
createBoard(@Body('title', ParameterPipe) title, @Body('description') description): Board {
    return this.boardService.createBoard(createBoardDto);
}

Global Pipes
글로벌 파이프로서 애플리케이션 레벨의 파이프다.
클라이언트에서 들어오는 모든 요청에 적용이 된다.
가장 상단 영역인 main.ts에 넣어주시면 된다.

pipe 종류

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseFloatPipe
  • ParseEnumPipe

pipe를 사용하여 유효성 검사

#create-board.dto.ts

import { IsNotEmpty } from "class-validator";

export class CreateBoardDto {
    @IsNotEmpty()
    title: string;

    @IsNotEmpty()
    description: string;
}
# boards.controller.ts 보드 생성
//CreateBoardDto를 파이프로 사용

@Post('/')
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardService.createBoard(createBoardDto);
}

커스텀 파이프

지금까지는 NestJS에서 이미 구성해놓은 built-in 파이프를 사용했다.
하지만 이것 말고도 따로 생성해서 사용할 수 있는 CUSTOM PIPE도 있다.
커스텀 파이프를 만들어서 사용해보자.

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

import { PipeTransform, BadRequestException} from "@nestjs/common";
import { BoardStatus } from "../board.model";

export class BoardStatusValidationPipe implements PipeTransform{
    readonly StatusOptions = [
        BoardStatus.PRIVATE,
        BoardStatus.PUBLIC,
    ]

    transform(value: any){
        value = value.toUpperCase(); // 대문자로 변환 / private, public이 모두 대문자라서

        if(!this.isStatusValid(value)){
            throw new BadRequestException(`${value} isn't in the status options`);
        }
        return value;
    }

    private isStatusValid(status: any){
        const idx = this.StatusOptions.indexOf(status);
        return idx !== -1;
    }
}

커스텀 파이프 적용

@Patch('/:id/status')
updateBoardStatus(
    @Param('id') id: string,
    @Body('status', BoardStatusValidationPipe) status: BoardStatus
) {
    return this.boardService.updateBoardStatus(id, status);
}

0개의 댓글