nest의 공식문서를 토대로 작성합니다.
이론을 건너뛰고 싶으시면 여기로
컨트롤러는 들어오는 requests를 처리하고 response를 클라이언트에게 반환합니다.
컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 받는 것입니다. 각 컨트롤러에는 하나 이상의 경로가 있으며 각 경로는 다른 작업을 수행합니다.
기본 컨트롤러를 만들기 위해 클래스와 '데코레이터'를 사용합니다.
데코레이터는 클래스를 필요한 메타데이터와 연결하고 Nest가 라우팅 맵을 생성할 수 있도록합니다. (요청을 해당 컨트롤러에 연결)
HINT
유효성 검사가 내장된 CURD 컨트롤러를 빠르게 생성하려면 CLI의 CRUD generator를 사용할 수 있습니다.$ nest g resource [name]
(매우 편한 명령어이므로 이후에 생성할 때 더 자세히 보겠습니다.)
기본 컨트롤러를 정의하기 위해 필수인 @Controller() 데코레이터를 사용합니다.
자 이제 컨트롤러를 생성해 봅시다! CLI를 사용해서 만드는 법을 알려드릴게요.
우선 $ npm i -g @nestjs/cli
로 설치를 했다면 터미널에 $ nest
를 입력해 봅시다.
이렇게 어떤 명령어를 사용할 수 있는지, 단축어는 무엇인지, 무슨 역할을 하는지가 쭉 나옵니다!
지금 우리가 사용할 것은 generate 입니다.
$ nest generate controller [name]
$ nest g co [name]
두 개가 같은 명령어 입니다. 단축어로 짧게 타이핑이 가능합니다.
공식 문서를 따라 $ nest g co cats
로 컨트롤러를 생성해 봅시다.
이렇게 파일들이 생성, 업데이트 됩니다. cli의 편리성을 여기서도 확인할 수 있죠. 컨트롤러 파일과 컨트롤러 테스트 파일을 생성해주고
이렇게 app.module.ts
파일에 자동으로 생성한 컨트롤러를 넣어줍니다.
이제 공식 문서를 따라 cats.controller.ts
를 이렇게 작성합니다.
# cats.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
위처럼 @Controller() 데코레이터에 'cats'를 작성하면 관련된 경로들을 쉽게 그룹화하고 반복 코드를 최소화할 수 있습니다.
localhost:3000/cats
의 하위로 계속 경로를 만들 수 있다는 것이죠.
@Get() 데코레이터는 HTTP 요청 메소드입니다. 이 데코레이터에 아무것도 작성하지 않는다면 GET localhost:3000/cats
를 요청했을 때 findAll() 이라는 메소드가 호출되는 것입니다. 그리고 findAll()은 200 상태 코드 및 문자열을 반환하죠.
한번 확인해볼까요?
npm run start
로 앱을 실행하고 api 테스트기로 확인해보면
(저는 Insomnia를 사용했습니다)
200 상태코드와 문자열을 반환하는 것을 확인할 수 있습니다.
이제 Nest가 이렇게 응답을 조작하는 두 가지 옵션을 봅시다.
옵션 | |
---|---|
표준(권장) | 내장 메소드를 사용하여 요청 핸들러가 JS 객체 또는 배열을 반환하면 자동으로 JSON으로 직렬화. 하지만 JS 원시 타입(string, number, boolean..)을 반환하면 직렬화 없이 그냥 값만을 전달. 이게 응답 핸들링을 간단하게 해줌. 응답의 상태코드는 201을 사용하는 POST 요청을 제외하고 언제나 기본으로 200임. 우리는 이걸 @HttpCode(...) 데코레이터를 이용해 쉽게 변경할 수 있다. |
라이브러리 별 | @Res() 데코레이터를 사용해서 라이브러리 별 응답 객체를 사용할 수 있다. 이 방법을 사용하면 해당 객체에 의해 노출된 기본 응답 처리 방법을 사용할 수 있다. 예를 들어 Express에서 response.status(200).send() 와 같이 응답을 구성 가능. |
WARNING
Nest는 @Res() 또는 @Next()를 감지하면 라이브러리 별 옵션을 선택했다고 간주함. 즉 두 가지 방법을 동시에 사용하면 표준 방법은 자동으로 비활성화됨.
두 가지 방법을 동시에 사용하고 싶으면@Res({passthrough: true})
옵션을 주면 됨.
핸들러는 종종 클라이언트 요청 세부 사항에 접근합니다.
요청 객체에 @Req() 데코레이터를 사용하여 접근할 수 있습니다.
# cats.controller.ts
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';
}
}
HINT
express의 타이핑을 이용하려면 @types/express 패키지를 설치.
근데 제가 노마드코더 니꼬쌤 강의를 들어봤는데 이렇게 req
, res
같은 express
객체를 직접적으로 사용하는 게 좋은 방법은 아니라고 하더라구요.
그 이유는 Nest는 두 개의 프레임워크와 작동하기 때문인데요 기본적으로 express
위에서 실행되는데 이걸 Fastify
(express 보다 두 배 빠름)로 전환할 때 오류를 뱉어낼 수 있다고 합니다.
물론 필요하면 사용해도 상관 없구요ㅎㅎ
요청 객체는 HTTP 요청을 나타내는데 query, string, parameters, HTTP headers, body의 속성을 가집니다. 대부분의 경우 수동으로 이 속성을 가져올 필요가 없습니다. @Body() 또는 @Query() 데코레이터를 대신 사용할 수 있습니다.
제공되는 데코레이터를 살펴볼까요?
decorator | obj |
---|---|
@Request(), @Req() * | req |
@Response(), @Res() | res |
@Next() | next |
@Session | req.session |
@Param(key?: string) | req.params / 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 |
*
Express와 Fastify의 호환성을 위해 Nest는 @Res(), @Response() 데코레이터를 제공. @Res()는 단순히 @Response()의 단축어임. 두 개 다 응답 객체 인터페이스를 직접 노출함. 이걸 사용하려면 패키지 설치 필요. 메소드 핸들러에 @Res(), @Response()를 주입하면 해당 핸들러에 대해 Nest를 라이브러리 별 모드로 설정하고 응답을 직접 관리해야 함.
앞서 우리가 cats의 resource를 받아오는 GET 경로를 호출했다면 이번에는 새로운 resource를 생성하는 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는 모든 HTTP 표준 메소드에 대한 데코레이터를 제공합니다: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(). 그리고 @All()은 이 모든 데코레이터를 다루는 엔드포인트를 정의합니다.
패턴 기반 경로 또한 지원합니다. 예를 들어 asterisk(*
)가 wildcard로 사용되고 모든 문자의 조합으로 사용됩니다.
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
이 예처럼 만약 경로를 ab*cd
로 작성했다면 이는 abcd
, ab_cd
, abecd
등등이 모두 가능합니다. 문자 ?
, +
, *
, ()
는 경로에 사용될 수 있으며 정규 표현식의 하위 집합입니다.
-
, .
은 문자 그대로 해석됩니다. 와일드카드처럼 적용되지 않는다는 것이죠.
앞서 언급했듯이 201을 사용하는 POST 요청을 제외하고 기본적으로 응답은 모두 200 상태 코드를 사용함. @HttpCode(...) 데코레이터를 사용해서 핸들러 레벨에서 쉽게 변경할 수 있습니다.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
HINT
@nestjs/common 패키지에서 HttpCode를 import할 것.
상태코드는 정적이지 않지만 많은 요인에 의해 바뀝니다. 이 경우 라이브러리 별 응답 객체를 사용할 수 있습니다.(또는 오류가 발생한 경우 예외를 던질 수 있다)
사용자 정의 응답 헤더를 지정하려면 @Header() 데코레이터 또는 라이브러리 별 응답 객체를 사용할 수 있습니다(그리고 res.header()를 직접 호출).
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
HINT
@nestjs/common 패키지에서 Header import
특정 URL로 redirect 하려면 @Redirect() 데코레이터를 사용하거나 라이브러리 별 응답 객체(그리고 res.redirect()를 직접 호출)를 사용할 수 있습니다.
@Redirect()는 url, statusCode 선택적으로 두 가지 인수를 사용합니다. 생략할 경우 statusCode의 기본값은 302(Found)입니다.
@Get()
@Redirect('https://nestjs.com', 301)
때때로 HTTP 상태 코드 또는 redirection 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/' };
}
}
요청의 일부로 동적 데이터를 받아야할 때 정적 경로는 작동하지 않습니다. 파라미터와 함께 경로를 정의하기 위해 요청 URL에 있는 동적 값을 가져오기 위한 경로 파라미터 토큰을 추가할 수 있습니다. 이 방법으로 선언된 경로 파라미터는 @Param() 데코레이터를 사용하여 접근할 수 있습니다. 아래 예를 확인해 봅시다.
HINT
파라미터를 사용하는 경로는 정적 경로 뒤에 정의돼야 함. 이렇게 하면 파라미터화된 경로가 정적 경로로 향하는 트래픽을 가로챌 수 없음.
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
이처럼 params.id
를 통해 접근 가능합니다.
데코레이터에 특정 파라미터 토큰을 전달한 다음 메소드 본문에서 이름으로 직접 경로 파라미터를 참조할 수도 있습니다.
HINT
@nestjs/common 패키지에서 Param import
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
@Controller 데코레이터는 host
옵션을 사용하여 들어오는 요청의 HTTP 호스트가 특정 값과 일치하도록 할 수 있습니다.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
WARNING
Fastify는 중첩 라우터를 지원하지 앟으므로 하위 도메인 라우팅을 사용할 때는 Express 어댑터를 대신 사용해야 함.
host
옵션은 동적 값을 가질 수 있습니다. 아래처럼 사용 가능
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
다른 프로그래밍 언어 배경을 가진 사람들에게는 Nest에서 거의 모든 것이 들어오는 요청에 걸쳐 공유된다는 사실이 의외일 수 있습니다.
데이터베이스에 대한 연결 풀, 글로벌 상태를 가진 싱글톤 서비스 등이 있습니다.
Node.js는 모든 요청이 별도의 스레드에서 처리되는 Multi-Threaded Stateless 모델을 따르지 않습니다. 따라서 싱글톤 인스턴스를 사용하는 것이 완전히 안전합니다.
그러나 GraphQL 애플리케이션의 per-request 캐싱, request tracking 또는 multi-tenancy와 같이 컨트롤러의 요청 기반의 수명처럼 엣지 케이스가 있습니다.
엣지 케이스?
극단적인(최대, 최소) 작동 매개변수에서만 발생하는 문제라고 합니다.
우리는 최신 JS를 좋아하며 데이터 추출이 대부분 비동기인 것을 알고 있습니다. 그래서 Nest가 async
함수를 지원하고 잘 작동하는 이유입니다.
모든 async 함수는 Promise
를 반환합니다. 즉 Nest가 자체적으로 해결할 수 있는 지연된 값을 반환할 수 있습니다.
@Get()
async findAll(): Promise<any[]> {
return [];
}
위 코드는 완전히 유효합니다. 또한 Nest 경로 핸들러는 관찰 가능한 스트림 RxJS를 반환할 수 있어 훨씬 강력합니다.
@Get()
findAll(): Observable<any[]> {
return of([]);
}
위 두 가지 방법 모두 효과가 있으며 필요에 따라 사용하면 됩니다.
위에서 했던 POST에서 우리는 그 어떤 client params도 받지 않았습니다. @Body() 데코레이터를 추가해서 해결해 봅시다.
만약 TS를 사용한다면 DTO(Data Transfer Object) 스키마를 정의해야 합니다.
DTO는 네트워크를 통해 데이터를 전송하는 방법을 정의하는 객체입니다.
TS 인터페이스 또는 간단한 클래스를 사용하여 DTO 스키마를 생성할 수 있습니다.
우리는 클래스를 사용하는 것을 추천합니다. 왜냐면 클래스는 JS ES6 표준의 일부이므로 컴파일된 JS에서 실제 엔티티로 보존되기 때문입니다.
반면에 TS 인터페이스는 트랜스파일 중 제거되므로 Nest는 런타임에 해당 인터페이스를 참조할 수 없습니다.
파이프와 같은 기능은 런타임에 변수의 메타 타입에 접근할 수 있을 때 추가적인 가능성을 제공하기 때문에 중요합니다.
글이 좀 기네요 그냥 요약하자면 DTO를 만들 때는 클래스를 사용해라! 입니다. 😀
CreateCatDto
클래스를 생성합니다.
# create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
3개의 기본 속성만을 가집니다. 이후 CatsController에서 해당 DTO를 사용할 수 있습니다.
# cats.controller.ts
...
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
HINT
값에 대한 유효성 검사를 ValidationPipe를 통해 할 수 있는데 자세한 건 해당 챕터에서 보겠습니다.
에러를 다루는 챕터가 따로 있습니다.
나중에 같이 보겠습니다!
CRUD를 위한 컨트롤러의 예입니다. (위에서 create-cat.dto.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`;
}
}
HINT
Nest CLI는 모든 상용 코드를 자동으로 생성하는 generator를 제공하여 이 모든 작업을 피하고 개발자 경험을 훨씬 더 단순화할 수 있도록 도와줍니다. 자세한 건 여기로
나중에는 dto 폴더를 따로따로 관리하는 게 편합니다!
제 코드를 참조해보세요😎
https://github.com/cxzaqq/cxzaqq-velog/tree/2.2-controller
컨트롤러가 정의되어도 Nest는 CatsController가 존재하는지 모릅니다.
컨트롤러는 항상 모듈에 속하기 때문에 @Module() 데코레이터 안에 컨트롤러 배열을 포함해야 합니다.
하지만 우리는 이미 $ nest g co cats
로 컨트롤러를 생성했기 때문에 app.module.ts
에 이미 포함된 것을 확인할 수 있습니다.
@Modue() 데코레이터를 사용해서 모듈 클래스에 메타데이터를 첨부했고 Nest에서 어떤 컨트롤러를 마운트해야 하는지 쉽게 반영할 수 있습니다.
CatsController를 사용하려면 app.module.ts에 추가해줘야 한다는 말입니다.
아까 응답을 조작하는 두 가지 옵션이 있다고 했는데 여기서는 두 번째 옵션을 설명합니다.
저는 생략하겠습니다! 궁금하신 분들은 공식문서를 보시면 됩니다!
고생하셨습니다!
다음 글에서 만나요~~😀
저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!