컨트롤러 2편

자원

이전에는 cats 리소스 (GET 경로)를 가져오는 엔드 포인트를 정의했습니다. 여기에 더해 새로운 레코드를 생성하는 엔드 포인트도 제공하려고 합니다. 이를 위해 POST 핸들러를 생성 해 보겠습니다.

// cats.controller.ts

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는 @Post()외에도 모든 표준 HTTP 메서드에 대한 데코레이터를 제공합니다. @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head()가 있습니다. 추가적으로 @All()과 같이 모든 항목을 처리하는 엔드 포인트도 있습니다.

와일드 카드 라우팅

패턴 기반 경로도 지원됩니다. 예를 들어 별표는 와일드 카드로 사용되며 모든 문자 조합과 일치합니다.

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

abcd, ab_cd, abecd 경로는 'ab\*cd'경로 경로가 일치합니다. ?, +, *, 및 () 문자는 경로 정보에 사용될 수 있습니다. 얼핏보면 정규 표현식과 같다고 생각되겠지만 정규 표현식의 부분집합입니다. 정규 표현식과는 달리 하이픈(-)과 점(.)은 문자 그대로 경로로 인식합니다.

상태코드

언급했듯이 응답 상태코드 는 201POST 요청을 제외하고 기본적으로 항상 200 입니다. 핸들러 레벨에서 @HttpCode(...)데코레이터를 추가하여 이 동작을 쉽게 변경할 수 있습니다. HttpCode@nestjs/common에서 Import 할 수 있습니다. 아래 코드는 HTTP 상태코드 204를 반환합니다.

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

가끔씩 상태코드는 고정되지 않고 여러 요인에 따라 변하기도 합니다. 이 경우 주입된 @Res() 객체를 사용하여 직접 상태코드와 응답을 반환하시기 바랍니다.

헤더

사용자 지정 응답 헤더를 지정하려면 @Header() 데코레이터 혹은 라이브러리 특정 응답 객체를 사용할 수 있습니다. 예를 들어 res.header() 말이죠. Header@nestjs/common에서 import 할 수 있습니다.

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

리디렉션

응답을 특정 URL로 리디렉션하려면 @Redirect() 데코레이터 또는 라이브러리 특정 응답 객체를 사용합니다. 예를 들어 res.redirect()를 직접 호출 할 수 있습니다.

@Redirect()는 2개의 인수를 받습니다. url, statusCode 인데 모두 필수는 아닙니다. 생략 된 경우 statusCode의 기본 값은 302(Found) 입니다.

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

HTTP 상태 코드나 리디렉션 URL을 동적으로 확인해야하는 경우가 있습니다. 다음과 같은 형태로 경로 핸들러 메서드에서 객체를 반환하면됩니다.

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

반환 된 값은 @Redirect()데코레이터에 전달 된 모든 인수를 재정의합니다. 예를 들면

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

경로 매개변수

만약 여러분이 요청의 일부로 동적인 데이터를 받아야 할 경우(예: /GET /cats/11과 같은 정보)에는 정적인 경로로는 어떻게 할 수 없죠. 매개 변수가 있는 경로를 사용하기 위해서는 경로에 매개 변수 토큰을 추가하여 요청 URL의 해당 위치에서 동적 값을 잡아낼 수 있습니다. 아래 예제에서는 @Get() 데코레이터에서 경로 매개 변수 토큰을 어떻게 사용하는지 보여줍니다. 이런 방식으로 선언된 경로 매개 변수는 @Param() 데코레이터를 사용하여 접근할 수 있으며, 이는 메서드 서명에 추가되어야 합니다.

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

@Param() 데코레이터는 경로 매개 변수에 접근할 때 사용합니다. 위의 코드에서 볼 수 있듯이 params.id로 경로 매개변수 id를 참조할 수 있습니다. 아래의 예제처럼 특정 매개 변수 토큰을 데코레이터에 전달한 다음 메서드 본문에서 이름으로 바로 경로 매개 변수를 참조할 수도 있습니다. 경로 매개변수가 하나 있을 경우 저는 보통 아래와 같이 사용합니다.

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

Param@nestjs/common 패키지에서 import 할 수 있습니다.

하위 도메인 라우팅

@Controller 데코레이터는 host 옵션을 사용하여 들어오는 요청의 HTTP 호스트가 특정 값과 일치해야지 처리하는 옵션을 추가할 수 있습니다.

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

경로 매개변수처럼 host 옵션도 토큰을 사용하여 호스트 이름의 특정 위치에서 동적 값을 가져올 수 있습니다. 아래 예제의 @Controller() 데코레이터 내에 설정한 호스트 매개 변수 토큰을 참고하시기 바랍니다. 이러한 방식으로 선언한 호스트 매개 변수는 각 핸들러의 @HostParam() 데코레이터를 사용하여 접근 할 수 있습니다.

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

비동기

데이터를 추출하는 과정은 대부분 비동기적 입니다. Nest도 당연히 async 함수를 통해 이를 지원합니다.

모든 비동기 함수는 Promise를 반환해야 합니다. 즉, Nest가 자체적으로 해결할 수있 는 지연된 값을 반환 할 수 있습니다.

// cats.controller.ts

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

요청 페이로드

이전 예제의 POST 경로 핸들러(create())는 클라이언트로부터 어떠한 값을 받지 않았습니다. 이제 클라이언트로부터 값을 받아보죠. 함수 시그니쳐@Body() 데코레이터를 추가해보겠습니다.

그 전에 먼저 DTO(Data Transfer Object) 스키마를 정해야 합니다. DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체입니다. 타입스크립트 인터페이스를 사용하거나 단순한 클래스를 사용해서 DTO 스키마를 결정할 수 있습니다. Nest처럼 Express에 기반을 두고 그 위에 타입스크립트로 추상화한 여러 프레임워크가 있는데, 대부분은 인터페이스 대신 클래스를 사용합니다. 클래스는 Javascript ES6 표준의 일부이기 때문에 타입스크립트에서 자바스크립트로 컴파일(혹은 트랜스파일)해도 그 원형이 남아있는 반면, 타입스크립트의 인터페이스는 컴파일하면 그 원형이 사라집니다. 따라서 Nest는 런타임 중에 이를 참조할 수가 없습니다. 런타임에 변수의 메타 타입에 접근할 수 있어야 Pipe와 같은 기능이 제대로 작동할 수 있기 때문에 이는 매우 중요한 문제입니다.

그럼 CreateCatDto 클래스를 만들어 보겠습니다.

// create-cat.dto.ts

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

세 가지 기본 속성만 있습니다. 이제 CatsController 안에 새로 만든 DTO를 사용해보죠.

// cats.controller.ts

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

오류 처리

여기에 오류 처리 (예: 예외 작업)에 대한 별도의 장이 있습니다. 이 부분은 아직 작업하지 않아서 추후 링크를 달도록 하겠습니다.

전체 리소스 샘플

아래 예제는 컨트롤러에서 CRUD(Create, Read, Update, Delete)를 다루는 방법을 제시합니다.

// cats.controller.ts

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는 CatsController의 존재를 알지 못합니다. 이 클래스의 존재를 Nest에 알려야 Nest에서 CatsController의 인스턴스를 만들어서 클라이언트의 요청을 처리할 수 있습니다.

컨트롤러는 항상 모듈에 속하므로 컨트롤러가 있다면 @Module() 데코레이터 안에 controllers 배열에 컨트롤러를 포함해야 합니다. 아직 예제에는 루트인 AppModule 이외에 다른 모듈을 생성하지 않았기 때문에. AppModuleCatsController를 등록하여 사용합니다.

// app.module.ts

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

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

@Module() 데코레이터를 사용하여 모듈 클래스에 메타 데이터를 첨부했으며 Nest는 이제 어떤 컨트롤러를 마운트해야하는지 쉽게 반영 할 수 있습니다.

라이브러리별 접근 방식

지금까지 응답을 조작하는 Nest 표준 방식에 대해 논의했습니다. 응답을 조작하는 두 번째 방법은 이전에도 설명했다시피 라이브러리별 응답 객체를 사용하는 것 입니다. 특정 응답 객체를 삽입하려면 @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([]);
  }
}

이 방식은 그래도 잘 작동하긴 하지만, 응답 객체(헤더 조작, 라이브러리 별 기능 등)에 대한 모든 제어권을 다루기 때문에 더 많은 유연성을 허용합니다. 권한이 크면 책임도 큰 법이기 때문에 주의해서 사용해야 합니다. 일반적으로 이 방식은 훨씬 덜 명확하고 몇 가지 단점이 있습니다. 가장 큰 단점은 코드가 플랫폼에 종속된다는 점입니다. Express 라면 Express, Fastify라면 Fastify 전용 코드를 사용하기 때문입니다. 나중에 Express에서 Fastify 혹은 그 반대로 하위 계층(웹 서버)을 변경한다면 코드를 다 뜯어 고쳐야 합니다. 또한 두 번째는 테스트하기가 어렵습니다. 응답 객체를 Mocking해야 하기 떼문입니다.

또한 위의 예제에서 인터셉터 및 @HttpCode()/ @Header()데코레이터와 같은 Nest 표준 응답 처리에 의존하는 Nest 기능과의 호환성이 사라집니다. 이 문제를 해결하려면 다음과 같이 passthrough 옵션을 true로 설정하시기 바랍니다.

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

이제 Nest 기본 응답 개체와 상호 작용할 수 있지만 (예: 특정 조건에 따라 쿠키 또는 헤더 설정) 나머지는 프레임워크에 맡깁니다.