javascript

추석하루 간단하게 본 NEST JS

윤-찬미 2021. 9. 22. 17:42

추석하루 집에서 누워있으려 했는데 그냥 그동안 호기심만 있던 nest를 간단하게 보고 정리를 좀 해보았다.

기술을 채택하기전 우리는 늘 "왜" 라는 물음에 답할 줄 알아야한다.

"왜" nest가 뜨게 됐고 핫한지 기존 express만 쓰던 nodeJS 개발자들이 "왜" nest를 쓰기 시작했는지?

나도 깊이본건 아니지만 간단하게 본 지나가는 사람으로서 본 바로는 참 구조가 잘 잡혀있다 라는 느낌을 받았다.

기존 express는 굉장히 프리덤 했다! 말그대로 아주 자유로운!

근데 nest 이자식 구조가 참 잘잡혀있고 마치 spring 같다.

nestJS는 컨트롤러, 모듈, 서비스 등 각각 역할이 분명하기때문에 그로인한 구조화된 작업 진행이 가능한 듯 보였다.


📌 정리

1. 기본 express와 간단 비교

따라서 express를 알고는 있어야한다.

  • express의 구조는 자유롭다.

    이건 장점이 될 수도 있고, 단점이 될 수도 있겠다.

    반면 nestJS는 어느정도 체계적인 구조를 따라가 개발을 진행한다.

    controller, service, module 클래스등등 모두 각각 역할을 가지고 있다.

    IoC와 DI 같은 디자인 패턴들을 도입해서 그것들에 맞추지 않으면 개발하기 힘든 구조다.

    하지만 오히려 어느정도 개발의 통일성을 가져올 수 있다.

    간략하게만 봤지만 spring이랑 굉장히 비슷한 구조를 가져가는 듯 하다.

  • typescript

    nestJS는 기본셋팅부터 타입스크립트 지원이 잘 되어있다.

    반면, express는 초기 셋팅부터 다 수동으로 해주어야한다는 불편한점이 있다.

 

2. nestJS setting

nest의 경우 셋팅은 공식문서를 보고 하는게 제일 빠르긴 하다.

간략하게 설명하면, create-react-app 과 같이 템플릿을 만들어주는

아래 명령어를 입력하자.

npm i -g @nestjs/cli
nest new project-name

Hot Reload도 셋팅해보자

npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
// 루트경로에 webpack-hmr.config.js 파일을 만들어 준다.
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename }),
    ],
  };
};
// main.ts
declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();
// package.json
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"
npm run start:dev

 

3. controller

라우터라 보면 된다. 들어오는 요청 을 처리 하고 클라이언트에 응답 을 반환 하는 역할을 한다.

기본 컨트롤러를 만들기 위해 클래스와 decorator를 사용한다. decorators는 클래스를 필수 metadata와 연결하고 Nest가 routing map을 생성할 수 있도록 한다.

간단하게 공식문서에나온 예제로 예시를 들면 @Controller('cats') 에 넣은 prefix 가 클래스 내부에서 @Get 데코레이터(에노테이션)와 만나 GET /cats 메서드가 만들어지고 findAll함수와 자동 맵핑 된다.

(이걸 다 nest에서 해줌!)

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get() // GET /cats
  findAll(): string {
    return 'This action returns all cats';
  }
}

그냥 궁금한거..

아니 근데 사실 난 데코레이터 여기서 좀의아했던게 겨우함수일 뿐인데 어떻게 맵핑이 되는거지? 라는 생각이 들었다. (또나왔다 왜충 찬미)

Reflect metadata랑 데커레이터로 클래스랑 메서드에 달린 정보 담아뒀다가 모듈에 provide하면 거기서 읽어서 nest에 올린 http 프레임워크에 연결해서 돌아가는 방식이다!(희희 나도 몰라서 물어보고 설명들음)

갑자기 얘기가 삼천포로 빠짐..

아무튼 위처럼 컨트롤러를 만들면, 아래 컨트롤러에 컨트롤러를 추가해주면 된다!

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

@Module({
  imports: [],
  controllers: [AppController], // <-- 여기
  providers: [AppService],
})
export class AppModule {}

 

4. service

nestJs에는 서비스 라는 개념도 있는데, 처음에는 사실 잘 와닿지 않았다.

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

그냥 컨트롤러만 있으면 되는거 아닌가? 그래서 공식 문서를 다시 읽어 보았다.

In the previous chapter, we built a simple CatsController. Controllers should handle HTTP requests and delegate more complex tasks to providers. Providers are plain JavaScript classes that are declared as providers in a module. 
이전 장에서 우리는 간단한 CatsController를 만들었습니다. 컨트롤러는 HTTP 요청을 처리하고 더 복잡한 작업을 프로바이더에게 위임해야 합니다. 공급자는 모듈에서 provider로 선언된 일반 자바스크립트 클래스입니다.

라고 되어있었다. 즉 비지니스로직으로 분리하자 이거다.

뭐 말그대로 다때려박아도 되긴하는데 그렇게 치면 컴포넌트도 나눌필요 없고 사실상 모든 코드는 다 한 파일에 때려박아도 되는데 코드를 분리해서 유지보수나 관리 포인트를 분리해 생산성을 높이자는 거다.

컨트롤러는 요청과 응답에 집중하고,

서비스는 비지니스로직에 집중하자!

이렇게 하면 테스트 코드 짜기도 좋겠다!!!

 

5. provider

문서를 읽고 개발을 하다보면 provider라는 용어를 많이 볼 수 있다. 이게 무엇일까?

공식문서에 따르면 아래와 같이 설명하고 있다.

프로바이더는 Nest의 기본 개념입니다. 많은 기본 Nest 클래스는 서비스(Service), 레파지토리, 팩토리, 헬퍼 등등의 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 의존성을 주입할 수 있다는 점입니다. 이 뜻은 객체가 서로 다양한 관계를 만들 수 있다는 것을 의미합니다. 그리고 객체의 인스턴스를 연결해주는 기능은 Nest 런타입 시스템에 위임될 수 있습니다.

우선 사전에 몇가지 알아야 알 것이 있어 설명을 해보겠다.

 

- nestJS는 계층형구조 라는 아키텍쳐 디자인을 따르고 있다.

이는 응집도는 높이고 결합도는 낮추는 소프트웨어 설계다.

계층형 기법은 아래와 같이 나눌 수 있다.

  • Presentation Tier: 사용자 인터페이스 혹은 외부와의 통신을 담당
  • Application Tier: Logic Tier라고 하기도 하고 Middle Tier라고 하기도 함. 주로 비즈니스 로직을 여기서 구현을 하며, Presentation Tier와 Data Tier사이를 연결
  • Data Tier: 데이터베이스에 데이터를 읽고 쓰는 역할을 담당

Nest에서는 각 계층을 아래처럼 연결지어 보면 된다.

  • Presentation → 컨트롤러
  • Application → 서비스 (주로 비지니스 로직)

Nest는 이렇게 컨트롤러와 그 하위 계층을 프로바이더라는 이름으로 구분한다.

 

- ioc/di

그 다음 살펴볼 개념이 ioc/di 인데,  nest 관련해서 찾아보는 것 보다, spring 키워드 넣어서 찾아봤다.

"컨테이너" 라는 말이 나오는데, 여기서 컨테이너는 "객체들이 담긴 용기" 라고 보면 된다.

spring에서는 이를 "spring Container" 라고 부르고,

spring Container 에 담긴 객체를 "Spring bean" (스프링 빈) 이라고 부른다.

spring Container 는 Spring Bean들의 빈의 생성과 관계, 사용, 생명 주기를 관리한다.

이때 spring Container는 Spring Bean을 관리하기 위해 "IoC" (제어역전)이라는 개념이 등장했다.

외부에서 객체를 생성하고 생성자/수정자 방식을 이용하여 객체를 주입하는 방식 이다.

방식을 제어하는 주체가 역전되었다 하여 '제어역전(IoC)'라 부른다

즉 IoC란 인스턴스의 생성부터 소멸까지의 인스턴스의 생명주기 관리를 컨테이너가 대신 하는 것 이다.

 

DI란 (Dependency Injection) 의존성 주입이라는 말이다.

"의존성 주입"은 제어역전 이 일어날 때,

스프링(or nest 등)이 내부에 있는 객체들간의 관계를 관리할 때 사용하는 기법이다.

일반적으로 인터페이스를 이용해서 의존적인 객체의 관계를 최대한 유연하게 처리할 수 있도록 한다.

의존성 주입은 말 그대로 의존적인 객체를 직접 생성하거나 제어하는 것이 아니라,

특정 객체에 필요한 객체를 외부에서 결정해서 연결시키는 것을 의미한다.

그럼 이렇게 하는게 무엇이 좋은가? 하면, 재사용성이나 객체간의 의존도가 줄어 들게 된다.

왜냐면 객체생성을 사용하는 쪽에서 박아두는게 아니라 객체 생성은 외부에 위임하고,

외부에서 객체를 주입하기 때문 이다.

 

nest로 예를 들어보자.

아래와 같은 프로바이더가 있고,

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

@Injectable()
export class AppService {
  getHello() {
    return process.env.USER;
  }
}

아래와 같은 컨트롤러가 있다고 생각해보자

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello(); // 이부분 보세요.
  }
}

현재 constructor 에 appService가 어떤애인지 명시하지도 않았는데,

this.appService.getHello(); 를 호출 할 수 있다. 이게 어떻게 된일인가?

바로 이게 nest에서 알아서 의존성을 주입해준건데,

app.module에는 아래와 같은 코드가 적혀있다.

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})

보면 AppService를 providers에 넣었는데,

이 코드를 통해 nest가 providers에 있는 리스트를 보고 알아서 의존성을 주입해준 것이다.

 

즉 어떤 컴포넌트가 필요하며 의존성을 주입당하는 객체를 프로바이더라고 생각하면 된다.

그리고 Nest 프레임워크 내부에서 알아서 컨테이너를 만들어서 관리해준다.

6. nest는 이래요

  • 파일의 이름은 '.' 으로 연결한다. (ex) app.service.ts)
  • export default 보단 export 를 보통 쓴다.
  • interface 보다 class를 쓴다. 인터페이스경우 타입스크립트 컴파일이 끝나면 사라지니, 런타임에선 존재하지 않지만, class 경우 자바스크립트 여서, 런타임에서도 클래스가 계속 남아있어서 타입 검증 등을 수행할 수 있다.
    // dto 만드는 예시
    export class JoinRequestDto {
      public email: string;
    
      public nickname: string;
    
      public password: string;
    }​

7. cli로 만드는 모듈, 서비스, 컨트롤러

cli로 손쉽게 모듈과 서비스 그리고 컨트롤러 등등을 만들 수 있다.

nest g co <컨트롤러 이름>
nest g s <서비스 이름>
nest g mo <모듈 이름>

8. swagger

스웨거 문서도 자동으로 만들어준다. 완전 꿀

npm install --save @nestjs/swagger swagger-ui-express
// main.ts 에 아래 내용 추가

const config = new DocumentBuilder()
    .setTitle('찬미의 문서 입니다.')
    .setDescription('찬미의 nest study 문서입니다.')
    .setVersion('1.0')
    .addCookieAuth('connect.sid')
    .build();

const document =  SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

아래와 같이 세부 설명이나 예시 등을 더 디테일하게 적어둘 수 있다.

import { ApiProperty } from "@nestjs/swagger";

export class JoinRequestDto {
  @ApiProperty({
    example: 'valley@naver.com',
    description: '이메일',
    required: true
  })
  public email: string;

  @ApiProperty({
    example: 'valley',
    description: '닉네임',
    required: true
  })
  public nickname: string;

  @ApiProperty({
    example: '1234!',
    description: '비밀번호',
    required: true
  })
  public password: string;
}

 

관련 사항은 스웨거 문서 보고 하면 됨.