컨트롤러 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'
경로 경로가 일치합니다. ?
, +
, *
, 및 ()
문자는 경로 정보에 사용될 수 있습니다. 얼핏보면 정규 표현식과 같다고 생각되겠지만 정규 표현식의 부분집합입니다. 정규 표현식과는 달리 하이픈(-
)과 점(.
)은 문자 그대로 경로로 인식합니다.
상태코드
언급했듯이 응답 상태코드 는 201 인 POST 요청을 제외하고 기본적으로 항상 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/1
중 1
과 같은 정보)에는 정적인 경로로는 어떻게 할 수 없죠. 매개 변수가 있는 경로를 사용하기 위해서는 경로에 매개 변수 토큰을 추가하여 요청 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
이외에 다른 모듈을 생성하지 않았기 때문에. AppModule
에 CatsController
를 등록하여 사용합니다.
// 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 기본 응답 개체와 상호 작용할 수 있지만 (예: 특정 조건에 따라 쿠키 또는 헤더 설정) 나머지는 프레임워크에 맡깁니다.