[Nest] Nest 시작하기 (1) - 사용하는 이유, Controller, Provider, Modules

권준혁·2020년 12월 26일
3

nest

목록 보기
1/2
post-thumbnail
post-custom-banner

Nodejs로 서버를 구축하고자 할 때 가장 많이 사용하는 프레임워크는 단연 express다.
쉽고 빠르고 자유롭고 러닝커브도 적다.

자유도가 높은 탓에 개발하는 사람에 따라 코드의 구조가 각양각색이다.
심한 경우 index.js 하나에 모든 코드를 넣을 수도 있고, 어떤 사람은 프로젝트 규모가 작더라도 구조적으로 분리하는 것을 중요하게 생각할 수도 있다.
사람마다 중요하게 생각하는 관점이 다르고, 프로젝트에 따라, 프로젝트 기한이나 팀원 수 등 여러가지 변수에 따라 아키텍처는 달라진다.

그렇기 때문에 팀으로 프로젝트를 할 때, 아키텍처를 미리 정해두지 않고 하다보면 시간적인 비용이 발생하기도 한다. 사실 발생하기도 한다기 보다 필수적으로 정해야 한다.

아키텍처가 정해져있다는 것은 프로젝트 생성단계에서 발생하는 비용을 크게 줄여줄 수 있지만, 자유도가 줄어드는 만큼 좋은 아키텍처인지 여러 방향에서 고민해서 선택해야 할 것이다.

Nest는 테스트가 쉽고, 확장가능 하고, 느슨하게 결합되며, 유지보수가 쉬운 애플리케이션 만들기 위해 애플리케이션 아키텍처를 제공한다.

Nest는 typescript를 완벽하게 지원하며 순수 javascript도 사용할 수 있다.

프로젝트를 처음 생성하면 나타는 폴더 구조다.

src폴더 내부의 main.ts > app.module.ts > app.controller.ts > app.service.ts 순으로 알아보자

기본생성 파일 살펴보기

  • 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();

AppModule은 기본적으로 생성되는 root모듈이다. NestFactory객체가 nestjs가 제공하는 것인데, 첫 번째 인자로 루트모듈을 입력받고, 두 번째 인자로 옵션을 입력받으며, Nest애플리케이션 인스턴스를 반환하는 Promise객체를 반환한다.


  • app.module.ts

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

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

AppControllerAppService는 기본적으로 생성되는 컨트롤러와 서비스다.
Module데코레이터는 가장 아랫줄의 export하고 있는 AppModule을 꾸며준다.
@Module데코레이터가 AppModule을 정의하고 있다.

이 애플리케이션 또는 모듈이 무슨 기능을 하고있는지 보려면 controllers와 provider를 살펴봐야겠다.


  • app.controller.ts

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();
  }
}

@nestjs/common모듈에서 Controller와 Get을 import하고있다. 둘다 데코레이터로 사용하고 있고, Get은 당연히 HTTP메소드일 것이다.
데코레이터는 반드시 코드 바로위에 공백없이 작성해야한다.
기본 라우트에 Get요청이 들어올 경우 Controller 클래스에 contructor에서 정의한 appService의 getHello() 함수가 실행된다.

이이서 service를 봐야겠다.


  • app.service.ts

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

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

@nestjs/common에서 Injectable 데코레이터를 import하고 사용한다.
아까 Controller에서 사용했던 AppService의 getHello함수가 보인다.
정리해보면 Controller의 Get요청이 들어오면 GetHello가 실행되고 있는데,
여기에 로직을 작성하면 되겠다.


이어서 Nest 인스턴스를 만드는 필수 메타데이터에 대해 알아보자

Controller

라우트 컨트롤러는 클래스와 데코레이터를 이용해 생성한다.
@Controller()데코레이터가 꼭 필요하다.
데코레이터는 클래스를 필수 메타데이터와 연결하고 Nest가 라우팅 맵을 만들 수 있도록 한다.

cats.controller.ts

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

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

라우트를 그룹화 했다. cats를 Controller의 경로 접두사로 설정한 것이다.

express의 경우를 다시 생각해보자. 컨트롤러는 req, res, next를 인자로 갖는 함수였다.
Next도 이에 대응하는 데코레이터를 제공한다.

CLI를 사용한다면 아래 명령어를 이용해 cats컨트롤러를 만들 수 있다.

nest g controller cats


  • 와일드카드 라우팅

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

  • 상태코드

기본적으로 상태코드는 POST요청은 201이고, 나머지는 항상 200이다.
@HttpCode(...) 데코레이터를 이용하면 임의 지정할 수 있다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

또는 @Res 데코레이터를 이용해 응답객체에서 지정할 수 도 있다.


  • 헤더

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

마찬가지로 @Header() 데코레이터를 이용해서 지정할 수도 있고, res.header()처럼 응답객체로 지정할 수도 있다.


  • 리다이렉트

@Get()
@Redirect('https://nestjs.com', 301)

@Redirect를 사용하거나 res.redirect() 모두 사용할 수 있다.
@Redirect의 첫번째 인자는 url, 두 번째 인자는 선택적으로 http코드를 입력한다.
생략할 경우 두 번째인자는 302(Found) 가 된다.


  • 동적 리다이렉트

동적으로 Redirect를 구현하려면 아래 객체를 리턴하면된다.

{
 "url": string,
 "statusCode": number
}

예시코드

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

  • 파라미터

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Req를 이용하면 req.params로 접근할 수 있겠지만, 필요에 맞는 데코레이터를 사용하면, 명시적으로 이 엔드포인트가 사용하는 객체을 보여줄 수 있을 것이다.


  • 서브도메인 라우팅

@Controller 데코레이터의 host옵션을 이용할 수 있다.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

Provider

Provider는 Service를 포함해 repository, factory, helper 등이 될 수 있다.
Provider는 단순히 @Injectable() 데코레이터 주석이 달린 클래스를 말한다.

Controller에서와 마찬가지로 CLI명령어를 지원한다.

nest g service cats

가장 많이 사용하게 될 Service를 먼저 살펴보고 이 후 에 천천히 살펴봐야겠다.

Controller는 단순히 HTTP요청을 처리하고, 이 후의 과정은 Provider에게 위임해야한다.
(Nest가 객체지향적으로 설계할 수 있으므로, SOLID원칙을 다르는 것을 권장한다. 단일책임원칙과 관련있어 보인다.)

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

데코레이터 이름이 Injectable인 이유는 Nest가 Dependency Injection이라고 부르는 디자인 패턴을 기반으로 구축되었기 때문이다.

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

위 코드에서 contructor에 CatsService를 Dependency로 주입하고 있다.


Scope

응용프로그램의 수명주기(일반적으로 프로그램 시작과 종료)에 동기화 되어있다.
지금까지 의존성 주입 디자인패턴을 기반으로 애플리케이션을 구축했다. 애플리케이션이 부트스트랩되면 모든 의존성 주입된 Provider를 인스턴스화한다. 그리고 애플리케이션이 종료되면 모든 공급자가 삭제된다.
일반적으로 Nest가 관리하기 때문에 필요없지만
다만, Provider의 수명 scope를 지정하는 특수한 경우도 있다.

이번 포스팅은 간단히 훑어보는게 목적이므로 Custom Provider, Optional Provider, Property-based injection 등은 다음에 알아봐야겠다.


Module

애플리케이션은 하나 이상의 모듈을 가진다.
루트모듈은 애플리케이션 그래프를 만드는데 스타팅 포인트가 된다.

아키텍처 제공을 목적으로 하는 프레임워크인 만큼 Module역시 CLI로 자동생성 할 수 있다.

nest g module cats

모듈은 기본적으로 Providers를 캡슐화한다. 따라서 import나 export되지 않은 provider를 삽입할 수 없다.


  • 기능모듈 Feature modules

기능모듈은 CatsController나 CatsService 처럼 특정기능과 관련된 코드들을 말한다.
동일한 기능이라면 코드들을 체계적으로 관리하고 명확한 경계를 위해 같은 폴더에 관리해보자.
복잡성을 관리하고 SOLID원칙에 따라 개발하는 것을 위해 cats라는 폴더에 기능모듈들을 옮기고 관리 한다고 한다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
  • 루트모듈에서 사용하기

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

  • 공유모듈 Shared modules

모든 모듈을 기본적으로 공유모듈이다. 모듈에서 export해주기만 하면 다른 모듈에서 사용할 수 있다.
CatsModule을 import하는 모든 모듈은 CatsService에 접근할 수 있고, 동일한 인스턴스를 공유하게된다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

  • 모듈 다시 내보내기 module re-exporting

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

  • 전역모듈 Global modules

자주 사용하는 모듈은 중복코드가 발생할 수 있다. Nest는 모듈 범위내에서 Provider를 캡슐화 하는데, import하지 않으면 사용할 수 없다.
@Globa() 데코레이터를 이용하면 전역 모듈로 사용할 수 있다.

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

모든 모듈을 Global module로 만드는 것은 좋은 디자인 패턴이 아니다.


profile
웹 프론트엔드, RN앱 개발자입니다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2021년 10월 10일

감사합니다

답글 달기