Nestjs#1 Controller

Nestjs 공식문서를 번역한 내용입니다. 일부 오역, 의미 전달이 모호한 부분이 있을 수 있습니다.

컨트롤러

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

image.png

컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 수신하는 것입니다. 라우팅 메커니즘은 어떤 컨트롤러가 어떤 요청을 수신하는지 제어합니다. 종종 각 컨트롤러에는 둘 이상의 경로가 있으며 다른 경로는 다른 작업을 수행할 수 있습니다. 기본 컨트롤러를 만들기 위해 클래스와 데코레이터를 사용합니다. 데코레이터는 클래스를 필수 메타데이터와 연결하고 Nest가 라우팅 맵을 생성할 수 있도록 합니다(요청을 해당 컨트롤러에 연결).

라우팅

다음 예제에서는 기본 컨트롤러를 정의하는 데 필요한 @Controller() 데코레이터를 사용합니다. cats의 선택적 경로 경로 접두사를 지정합니다. @Controller() 데코레이터에서 경로 접두사를 사용하면 관련 경로 세트를 쉽게 그룹화하고 반복적인 코드를 최소화할 수 있습니다. 예를 들어 경로 /customers 아래에서 고객 entity와의 상호 작용을 관리하는 경로 집합을 그룹화 할 수 있습니다. 이 경우 파일의 각 경로에 대해 경로의 해당 부분을 반복할 필요가 없도록 @Controller() 데코레이터에서 경로 접두사 customers을 지정할 수 있습니다.

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

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

CLI를 사용하여 컨트롤러를 생성하려면 $ nest g controller cats 명령을 실행하기만 하면 됩니다.

findAll() 메서드 앞의 @Get() HTTP 요청 메서드 데코레이터는 Nest에 HTTP 요청에 대한 특정 Endpoint에 대한 Handler를 생성하도록 지시합니다. 이 Endpoint는 HTTP 요청 메소드(이 경우 GET) 및 route path에 해당합니다. route path가 무엇일까요? route path는 Controller의 데코레이터에 선언된 접두어와 메소드 데코레이터의 경로를 연결하여 결정된 End point 경로 입니다. 본 예시에서 모든 경로(cats)에 대해 접두사를 선언했고 데코레이터에 경로 정보를 추가하지 않았으므로 Nest는 GET /cat 요청을 이 Handler에 매핑합니다. 언급했듯이 route path에는 선택적 컨트롤러 경로 접두사와 요청 메서드 데코레이터에 선언된 모든 경로 문자열이 모두 포함됩니다. 예를 들어, 데코레이터 @Get('profile')과 결합된 customer 경로 접두사는 GET /customers/profile과 같은 요청에 대한 경로 매핑을 생성합니다.

위의 예에서 이 Endpoint에 GET 요청이 발생하면 Nest는 요청을 개발자가 정의한 findAll() 메서드로 라우팅합니다. 여기서 선택하는 메서드는 완전히 임의의 이름을 가질 수 있습니다. 경로를 바인딩할 메서드를 반드시 선언해야 하지만 메서드의 이름은 아무런 의미를 가지지 않습니다.

이 메서드는 200 상태 코드와 응답을 반환합니다. 이 예에서는 응답값은 단순 문자열입니다. 왜 이런 결과가 나왔을까요? 설명을 위해 먼저 Nest가 응답을 조작하기 위해 두 가지 다른 옵션을 사용한다는 개념을 소개하겠습니다.

방식설명
표준 (권장)내장된 방식을 사용하면, 요청 핸들러가 Javascript 객체나 배열을 return 할때, 그것이 자동으로 JSON으로 직렬화 됩니다. 하지만 Javascript 기본 타입(e.g., string, number, boolean 등 )이 return 될때는 Nest는 직렬화 없이 값만 전달합니다. 이 방식은 응답 핸들링을 간편하게 만들어줍니다. 그저 값을 return하면 Nest가 그 이후를 모두 관리합니다. 이뿐 아니라, 응답의 상태코드는 기본값으로 항상 200이고, POST 요청에 대해서는 기본값이 201입니다. 핸들러 레벨에서 HttpCode(...) 데코레이터를 추가함으로써 이것을 쉽게 변경할 수 있습니다.
라이브러리 특화메소드 핸들러의 파라미터(e.g., findAll(@Res() response)에서 @Res() 데코레이터에 의해서 주입될 수 있는 특정한 라이브러리(express 등)의 응답 객체를 사용할 수 있습니다. 이 접근 방식을 사용하면 해당 객체에 의해 노출된 기본 응답 처리 메서드를 사용할 수 있습니다. 예를 들어 Express를 사용하면 response.status(200).send()와 같은 코드를 사용하여 응답을 구성할 수 있습니다.

Nest는 핸들러가 @Res() 또는 @Next()를 사용할 때를 감지하여 라이브러리 특화 옵션을 선택했음을 인식합니다. 두 접근 방식을 동시에 사용하면 이 단일 route path에 대해 표준 방식이 자동으로 비활성화되고 정상적으로 동작하지 않습니다. 두 접근 방식을 동시에 사용하려면(예: 쿠키/헤더만 설정하고 나머지는 프레임워크에 남겨두도록 응답 개체를 주입하여) @Res({ passthrough: true }) 처럼 passthrough 옵션을 반드시 true로 설정해야합니다.

요청 객체(Request Object)

핸들러는 종종 클라이언트 요청의 세부 정보에 액세스해야 합니다. Nest는 기본 플랫폼의 요청 객체(Request Object)에 대한 액세스를 제공합니다(Express 기본). 핸들러의 파라미터에 @Req() 데코레이터를 추가하여, Nest에 요청 객체를 삽입하도록 지시하는 방식으로 요청 객체에 액세스할 수 있습니다.

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

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

express 타입의 이점을 활용하려면 (위의 request: Request 파라미터 예제와 같이), @types/express 패키지를 설치하십시오.

요청 객체는 HTTP 요청을 나타내며 요청 쿼리 문자열, 매개변수, HTTP 헤더 및 본문에 대한 속성을 가지고 있습니다(자세한 내용은 여기 참조).

데코레이터객체
@Request(), @Req()req
@Response(), @Res()*res
@Next()next
@Session()req.session
@Param(key?: string)req.param/req.params[key]
@Body(key?: string)req.body/req.body[key]
@Query(key?: string)req.query/req.query[key]
@Headers(name?: string)req.headers/req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

Nest의 기본 HTTP 프레임워크(예: Express 및 Fastify)에서 타입 호환성을 위해 Nest는 @Res()@Response() 데코레이터를 제공합니다. @Res()는 단순히 @Response()의 별칭입니다. 둘 다 기본 기본 프레임워크의 응답 객체 인터페이스를 직접 노출합니다. 그것들을 사용할 때, 최대한 활용하려면 기본 라이브러리(예: @types/express)에 대한 타입도 가져와야 합니다. 핸들러 메소드에 @Res() 또는 @Response() 데코레이터를 사용하게 될때, 라이브러리 특화 모드를 사용하도록 설정하게 되는 것을 의미하고, 당신이 응답에 대한 책임을 관리할 책임이 있다는 것을 의미합니다. 이렇게 사용할 때, 당신은 반드시 응답 객체를 사용하여 응답을 해야합니다. (e.g., res.json(...) 또는 res.send(...)) 그렇지 않으면 HTTP 서버는 요청에 대한 응답을 하지 않고 멈추게 됩니다.

커스텀 데코레이터를 만드는 것을 배우고 싶으면 이 곳을 참고 하세요.

리소스(Resources)

앞서서, 우리는 고양이(cats)리소스(GET route)를 가져오기 위한 Endpoint를 정의했습니다. 또한 새로운 데이터를 생성하는 Endpoint를 제공하기를 원하는 상황입니다. 이것을 위해서 우리는 POST 핸들러를 만들어 보겠습니다.

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

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

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

간단합니다. Nest는 표준 HTTP 메소드를 위한 데코레이터를 제공합니다: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(). 그리고 @All()은 모든 메소드를 핸들링할 수 있는 Endpoint를 정의합니다.

와일드 카드(Route wildcards)

패턴 기반 라우트 또한 지우너합니다. 예를 들어, 아스트리크(*)는 와일드카드로 사용되며, 모든 문자 조합을 허용합니다.

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

ab*cd 경로는 abcd, ab_cd, abecd 등과 동치입니다. 문자 ?, +, () 도 라우트 경로에 사용될 수 있고 정규표현식의 부분입니다. 하이픈(-)과 점(.)은 문자 그대로 문자열 기반 경로로 해석됩니다.

상태 코드(Status Code)

앞서 언급과 같이 기본 상태코드는 200입니다. POST 요청의 경우는 예외적으로 201입니다. @HttpCode(...) 데코레이터를 사용하여 쉽게 변경할 수 있습니다.

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

HttpCode@nestjs/common으로부터 import 합니다.

종종 상태코드는 정적이지 않고 동적인 상황에 의존될 수 있습니다. (오류나 예외처리 시) 이러한 경우에는 라이브러리 특화 응답을 이용할 수 있습니다. (@Res() 주입)

헤더(Headers)

커스텀 응답헤더를 사용하기 위해서 당신은 @Header()를 사용하거나 라이브러리 특화 응답 객체(res.header()를 직접 호출)를 사용할 수 있습니다.

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

Header@nestjs/common에서 Import합니다.

리디렉션(Redirection)

응답을 특정한 URL로 리디렉션하기 위해서, @Redirect() 데코레이터를 사용하거나 라이브러리 특화 모드에서 응답 객체(res.redirect() 를 직접 호출)를 사용할 수 있습니다. @Redirect()urlstatusCode 두 개의 옵셔널 인자를 가지고 있습닙다. 기본 응답 코드는 302(Found) 입니다.

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

HTTP 상태코드나 URL을 동적으로 결정하고 싶다면, 라우트 핸들러 메소드가 아래와 같은 형태로 return 하도록 하면 됩니다:

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

리턴된 값은 @Redirect() 데코레이터의 인자로 전달되어 override됩니다:

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

라우트 파라미터(Route Parameter)

동적 데이터를 포함한 요청(e.g., GET /cats/1 id가 1인 고양이)을 할 때, 정적 라우트 경로는 동작하지 않습니다. 동적 파라미터가 포함된 라우트를 정의하려면, 요청 URL에서 동적인 값을 인식하기 위해서 파우트 파라미터 토큰을 추가 할 수 있습니다. 예제에서 @Get() 데코레이터에 매개변수 토큰을 사용하였습니다. 이렇게 정의된 라우트 파라미터에 @Param() 데코레이터를 통해서 접근할 수 있고, 핸들러 메소드의 파라미터에 추가되어야 합니다.

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

@Param()은 메소드의 파라미터(예제의 경우에는 params)를 위해 사용되고, 메소드 내부에서 사용될 수 있도록 메소드의 파라미터 속성으로 만들어 줍니다. 위의 코드에서 볼 수 있듯이 params.id를 참조하여 id 매개변수에 액세스할 수 있습니다. 데코레이터에 특정한 파라미터 토큰의 이름을 전달하면, 핸들러 메소드의 내부에서 바로 파라미터 이름에 접근할 수 있습니다.

Param@nestjs/common 패키지에서 Import 됩니다.

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

서브 도메인 라우팅(Sub-Domain Routing)

@Controller 데코레이터는 요청하는 HTTP 호스트가 특정 값과 일치하도록 요구하는 호스트 옵션을 사용할 수 있습니다.

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

Fastify는 중첩 라우터를 지원하지 않기 때문에, 서브 도메인 라우팅을 사용해야한다면 Express 어탭터를 사용사용해야합니다.

라우트 경로와 유사하게, 'host'옵션도 호스트의 이름의 특정 위치에서 동적경로 인식하기 위해서 토큰을 사용할 수 있습니다. 아래 @Controller() 데코레이션 예제와 같이 토큰을 사용할 수 있습니다. 핸들러 메소드에 선언된 @HostParam() 데코레이터를 통해서 호스트 파라미터에 접근 할 수 있습니다.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

스코프(Scope)

요청(reqeust)에 대해서 거의 모든 것이 공유된다는 사실이 다양한 프로그래밍 언어와 프레임워크를 경험한 사람들에게는 예상치 못한 일이 될 수 있습니다. Nest는 DB Pool, 전역 싱글톤 서비스 등 모든 것을 공유합니다. Node.js는 모든 요청이 별개의 스레드에 의해 처리되는 다중 스레드 모델을 따르지 않습니다. 따라서 싱글톤 인스턴스를 사용하는 것이 Nest Application에 안전합니다.

그러나 GraphQL 애플리케이션의 요청별 캐싱, 요청 추적, 멀티 테넌시 등에 대해서는 요청 기반 라이프 사이클을 가지는 컨트롤러가 필요 할 수 있습니다. 자세히 알아보려면 이 곳을 참고 하십시오.

비동기성(Asynchronicity)

우리는 모던 자바스크립트를 좋아하고 데이터가 대부분 비동기적으로 추출된다는 것을 알고 있습니다. 그래서 Nest는 async 함수를 지원하고 잘 동작하도록 합니다.

async/await에 대해서 더 알고 싶으면 이 곳 참조

모든 async 함수는 Promise를 반환합니다. 이것은 당신이 지연된(deferred) 값을 반환할 수 있고 Nest는 스스로 그것을 인식하여 해결할 수 있음을 의미합니다. 아래 예시 참고:

@Get()
async findAll(): Promise<any[]> {
  return [];
}

위 코드는 완전히 유효한 코드입니다. 뿐만 아니라, Nest 라우트 핸들러는 RxJS observable stream을 반환할 수 있어서 강력합니다. Nest는 자동으로 구독(subscribe)하여 대기하다가 스트림이 종료되면 마지막 값을 가져옵니다.

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

위 두 접근은 모두 정상적으로 동작하며, 필요한 요구사항에 따라 적절히 선택할 수 있습니다.

요청 데이터(Request Payloads)

앞선 POST 라우트 핸들러 예제에서는 클라이언트 파라미터들을 아무것도 받지 않았습니다. @Body() 데코레이터를 이용해서 이것을 고쳐봅시다.

하지만 먼저 우리는 DTO(Data Transfer Object)의 스키마를 결정해야합니다. DTO는 네트워크 너머로 어떻게 데이터가 전달되지를 정의하는 객체 입니. DTO 스키마는 Typescirpt 인터페이스나 간단한 클래스를 이용하여 정의 할 수 있습니다. 흥미롭게도 우리는 클래스 방식을 권장합니다. 왜냐고? 클래스는 Javascript ES5의 표준이기 때문에 컴파일된 Javascript에서 엔티티 그대로 보존됩니다. 반면 Typescript 인터페이스는 컴파일(변환) 중에 제거되기 때문에 Nest는 런타임에 실제로 인터페이스를 사용하지 않습니다. 이것은 런타임에 파이프와 같은 기능이 변수의 메타타입에 액세스할 때 추가 가능성을 가능하게 하기 때문에 중요합니다.

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

기본적인 세 개의 속성이 있습니다. 그리고 우리는 CatsController 내부에서 DTO를 사용할 수 있습니다.

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

ValidationPipe는 메서드 핸들러가 수신하지 않아야 하는 속성을 필터링할 수 있습니다. 이 경우 허용되는 속성을 허용 목록에 추가할 수 있으며 허용 목록에 포함되지 않은 속성은 결과 개체에서 자동으로 제거됩니다. CreateCatDto 예에서 허용 목록은 이름, 연령 및 품종 속성입니다. 여기에서 자세히 알아보세요.

오류 핸들링(Handling errros)

오류 핸들링은 별도로 이곳에서 다룹니다.

전체 리소스 샘플

아래는 여러 데코레이터를 사용해서 기본 컨트롤러를 만든 예제 입니다. 이 컨트롤러는 내부 데이터를 다루기 위한 여러 메소드들을 외부로 노출 시켜 줍니다.

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

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

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Nest CLI는 자동으로 보일러플레이트 코드를 생성해주는 기능을 제공합니다. 이를 통해 개발자에게 더 간편한 기능을 제공합니다. 이곳에서 참고 하세요.

시작해보기(Getting up and running)

컨트롤러를 완전히 정의했지만 Nest는 여전히 CatsController의 존재를 모르고, 결과적으로 이 클래스의 인스턴스를 생성하지 않습니다.

컨트롤러는 항상 모듈에 속하므로 @Module()데코레이터 내에 controller 배열에 포함합니다. AppModule 외에는 다른 모듈은 정의하지 않았기 때문에 CatsController를 인식시키기 위해서 AppModule을 이용합니다.

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

@Module({
  controllers: [CatsController],
})
export class AppModule {}

@Module()데코레이터를 이용해서 메타데이터를 모듈 클래스에 attach하였습니다. 이제 Nest는 장착해야하는 컨트롤러를 쉽게 반영할 수 있습니다.

라이브러리 특화 접근법(Library-specific approach)

지금까지 우리는 Nest 표준 response 조작 방법을 다루었습니다. response 조작 방법의 두 번째 방법으로 라이브러리 특화된 방법이 있습니다. 응답객체(response object)를 주입하기 위해서 @Res() 데코레이터를 이용합니다. 차이점을 확인하기 위해서 CatsController를 작성해봅시다:

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

이러한 접근 방식은 실제로 응답 객체를 완전히 접근할 수 있도록 제공하므로 어떤 면에서는 더 많은 유연성이 있지만 주의해서 사용해야합니다. 사실 이 방식은 훨씬 불명확하고 몇 가지 단점이 있습니다. 가장 주요한 단점은 코드가 특정 플랫폼에 종속(라이브러리 기반 마다 응답객체 따라 다른 API가 다르기 때문에)된다는 것입니다. 그리고 테스트하기가 더 까다로워 집니다. 그리고 Interceptor@HttpCode()/@Header() 데코레이션 같은 Nest의 표준 응답 핸들링에 의존성이 있는 특성을 사용하지 못하게 됩니다. passthrough 옵션을 true로 사용하여 허용할 수도 있습니다.

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

이제 기본 응답 객체(라이브러리 특화 객체)를 통해 응답 컨트롤을 할 수 있지만 나머지 동작은 프레임워크에 위임할 수 있습니다.