Nest.js/OVERVIEW

Controllers

atmosg 2023. 7. 13. 19:13

Controllers

컨트롤러는 클라이언트의 request를 핸들링하여 response를 리턴하는 기능을 담당한다. 라우팅 메커니즘이 어떤 컨트롤러가 어떤 request를 담당할지 컨트롤하는데, 각 컨트롤러는 통상 하나 이상의 라우트를 가지며 각 라우트가 각각 다른 작업을 수행한다.

 

기본적인 컨트롤러 생성을 위해 클래스와 데코레이터를 사용한다. 데코레이터는 필요한 메타데이터를 클래스와 연동시켜주며, 네스트로 하여금 라우팅 맵(각각의 request와 이를 담당하는 컨트롤러를 묶어내는 것)을 그려내도록 한다위 프로세스는 아래의 과정을 거쳐 동작하게 된다.

 

HINT
validation  기능이 내장된 CRUD 컨트롤러를 쉽게 만들어주는 CLI 명령어도 있다.
 - nest g resource [ name ]

 

 

Routing

위에서 언급했듯, @Controller() 코레이터를 사용해 기본적인 컨트롤러를 생성한다. 이 때 request 경로를 나타내는 접두사를 @Controller() 데코레이터에 명시해주면 관련 라우터들을 쉽게 그룹화할 수 있고, 코드 반복을 줄여준다.

 

예를 들어, 고양이와 관련된 라우터들을 그룹화하고 /cats 라는 경로하에 관리하고자 한다면 @Controller('cats')로 명시해주기만 하면 된다. 이후엔 /cats 경로를 반복해서 작성할 필요도 없어진다.

cats.controller.ts

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}
HINT
CLI로 컨트롤러를 만들고 싶다면
 - nest g controller cats

 

findAll() 메서드 앞의 @Get() 데코레이터는 HTTP request method 데코레이터로, 특정 엔드포인트로 들어오는 HTTP GET 요청을 처리하는 핸들러를 만들기 위해 사용된다. 라우트 경로는 컨트롤러에서 명시한 접두사와 메서드 데코레이터에 명시한 접두사를 연결한 경로로 지정된다. 

이 경우엔 @Controller('cat')@Get()을 합쳐서 Get /cats 가 라우트 경로가 되는 것이며, @Controller(customers')@Get('profile')이 있었다면 GET /customers/profile이 됐을 것이다.

현재 findAll() 메서드는 요청에 대한 응답으로 상태 코드 200과 함께 문자열을 리턴할 것이다. 어떻게 이게 가능한 것일까? 이에 대한 설명에 앞서 네스트가 response를 조작하기 위해 제공하는 두 가지 옵션에 대해 먼저 알아보자.

 

Standard
(recommended)
네스트에 내장된 메서드를 사용하면 request 핸들러가 object나 array를 리턴할 시 자동으로 JSON 형태로 직렬화(serialize)해준다. 

만약 리턴 값이 string, number, boolean 같은 자바스크립트 원시 타입이라면 직렬화 없이 해당 값을 그대로 내보낸다. 따라서 상황에 맞게 값만 리턴하면 나머지는 네스트가 적절히 형태를 갖춰 response를 내보내준다.

추가적으로 response의 기본 상태 코드값은 200이며, POST의 경우만 201로 지정돼있다. 만약 상태코드를 변경하고자 한다면 @HttpCode(...) 데코레이터를 사용하면 된다.
Library-specific Express와 같은 특정 라이브러리/프레임워크의 response 객체를 사용한다. 

메서드 핸들러의 @Res() 데코레이터에 intected될 수 있는데, findAll(@Res() response) 와 같은 형태로 사용하게 된다. 이 방법을 쓰면  익스프레스에서 쓰던 response.status(200).send() 형태 처럼 response 객체와 관련된 기본 핸들링 메서드를 사용할 수 있다
WARNING
만약 두 가지 방식이 동시에 사용되면 표준 방식은 자동으로 비활성화되고, 해당 라우터는 더이상 원하는대로 작동하지 않게 된다. 그래도 굳이 두 가지 방식을 동시에 쓰고싶다면 @Res() 데코레이터의 passthrough 옵션을 true로 설정해주면 된다 ( @Res({ passthrough: true }) )

 

 

Getting up and running

위에서 컨트롤러를 정의해주긴 했지만 네스트는 아직 CatsController라는 컨트롤러가 존재하는지 모르는 상태이고, 따라서 클래스의 인스턴스도 만들어지지 않은 상태이다(즉 동작하지 않음)

app.module.ts

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

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

 

컨트롤러는 항상 모듈 내부에 속해야 하므로 @Module() 데코레이터의 controllers 어레이에 포함시켜줘야 한다. 이렇게 하면 모듈 클래스에 메타데이터를 붙이게 되고, 그제서야 비로소 네스트가 어떤 모듈에 어떤 컨트롤러가 장착될 것인지 반영할 수 있게 된다.

 

 

Request object

라우트 핸들러를 만들다 보면 클라이언트가 보낸 request의 detail에 접근할 일이 제법 있다. 이를 위해 네스트는 기본 플랫폼(default: Express)의 request object에 접근하는 방법을 제공한다. 즉, 핸들러에 @Req() 데코레이터를 추가해서 request object를 주입(inject), 접근할 수 있게 된다.

   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 패키지를 설치해서 쓰자

 

request object는 HTTP 요청에 대한 쿼리 스트링, 파라미터, 헤더, 바디 등등의 프로퍼티들을 갖고 있다. 그런데 사실 네스트에선 이를 한땀한땀 손으로 접근할 필요가 전혀 없다. 네스트엔 이미 request object의 프로퍼티들을 취득하기 위한 데코레이터가 대부분 마련되어 있다.

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

 

기본 HTTP 플랫폼(Express, Fastify)과의 타입 호환성을 위해 네스트는 @Res()  @Response() 데코레이터를 제공한다(@Res는 @Response의 별칭일 뿐임) 둘 모두 원래 플랫폼의 response object를 노출시키는데, 이를 사용하고 싶다면 타입과 관련된 패키지를 설치해서 쓰도록 하자(e.g., @types/express)

단, @Res @Response 데코레이터 사용시 해당 핸들러에 한하여 Nest를 Library-specific mode로 설정하는 꼴이 되므로 response 관리에 주의하도록 해야한다. 즉, 서버가 멈추지 않도록 res.json() 이나 res.send( ) 등의 서버응답부터 시작해서 이런저런 관리가 필요하다.

 

 

Resources

앞에서 cats 리소스를 얻기 위한 엔드포인트 (GET route)를 @Get 데코레이터로 정의했었다. 비슷한 방식으로 표준 HTTP 메서드를 정의하기 위한 데코레이터들 또한 마련되어 있다. ( @Post() @Put() @Delete @Patch @Options() @Head() )

 

 

Route wildcards

패턴 기반의 라우트 경로를 지정할 수도 있는데, 예를 들어 * 는 와일드카드로써 모든 문자의 조합에 대응된다. 만약 'ab*cd'  라는 라우트 경로가 있으면 abcd ab_cd abecd 등등에 대응될 것이다. 이 외에도 정규표현식의 서브셋으로써  ?    + ()   도 존재한다. 단,  -   .  은 문자열 그대로 반영된다.

 

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

 

 

Status code

POST 요청에 대한 기본 상태 코드값은 201, 그 외에는 200으로 지정돼있다. 만약 상태 코드값을 바꾸고 싶다면 핸들러 레벨에서 @HttpCode(...) 데코레이터를 사용하면 된다.

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

 

그런데 상태 코드는 정적으로 정해진게 아니라 상황에 따라 값이 달라지는게 일반적인데, 이를 위해 library-specific response (inject using @Res() )을 사용할 수 있다. (혹은 에러 발생 상황이라면 예외 처리를 하던가)

 

 

Headers

response에 헤더를 작성하고 싶다면 @Header() 데코레이터를 사용하거나 library-specific response object를 사용하면 된다 ( res.header() )

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

 

 

Redirection

특정 URL로 리다이렉션 시키는 response를 응답하고 싶다면 데코레이터를 사용할 수 있다. 

 

@Redirect()은 두 가지 optional 인수를 받으며( url , statusCode ) statusCode생략 시 default 값은 302(Found) 만약 HTTP 상태 코드값과 redirect URL이 동적으로 결정돼야 하는 상황이라면, 라우트 핸들러에서 아래 모양의 객체를 리턴하면 된다.

{
  "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/' };
  }
}

 

 

Route parameter

request URL에 담긴 파라미터(예를 들어, GET /cats/12)를 받아오고 싶다면 라우트 경로에 파라미터 토큰 token을 추가할 수 있다. 아래 예시는 @Get() 데코레이터에 라우트 파라미터 토큰을 추가한 내용이며, 이렇게 선언된 파라미터 값은 @Param() 데코레이터를 사용해 메서드 레벨에서 접근할 수 있다.

// params 객체를 통째로 가져오거나
@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

// 원하는 속성값만 가져올 수도 있음
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

 

 

Sub-Domain Routing

@Controller 데코레이터에 host 옵션을 전달해서 들어오는 request의 HTTP host를 검증할 수 있다. route path와 유사하게 hosts 옵션 또한 token을 받아서 동적으로 값을 처리할 수 있으며, 메서드 레벨에서 데코레이터를 사용해 해당 값에 접근할 수 있다.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}
WARNING
Fastify는 서브도메인 라우팅에 관해선 지원이 빈약하므로 Express 어댑터를 사용하는게 차라리 낫다.

 

 

Scopes

네스트에선 거의 모든 것들이 incoming request 사이에서 공유된다. 전역적으로 사용되는 DB에 접근하는 커넥션 풀이라던가 싱글톤 서비스가 그 예시이다(물론 사례를 들자면 더 많겠지만)

 

다른 언어를 쓰다가 왔으면 이게 뭔 짓인가 싶겠지만, 애초에 Node.js 자체가 멀티스레드의 stateless model을 따르지 않는다. 때문에 네스트가 싱클톤 인스턴스를 사용하는 것이 의외로 safe 한 방식이다.

 

 

Asynchronicity

사실 JS로 데이터를 다루다 보면 어지간한 부분은 비동기로 처리해야한다. 따라서 네스트도 async 함수를 지원하며, 당연하지만 Promise를 리턴해야 한다.

cats.controller.ts

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

 

여기서 더 나아가 네스트는 RxJS의 observable streams을 리턴할 수 있는데, 자동으로 source를 subscribe하고 마지막에 emitted 되는 value를 취득할 수도 있다. (사실 RxJS를 잘 모르면 무슨 말인지 모를 테지만 RxJS는 꼭 공부해보길 권장함)

cats.controller.ts

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

 

 

Request payloads

POST 요청 같은 경우 당연히 request에 data가 딸려오게 되는데(request payload) 이 데이터를 취득하기 위해 @Body() 데코레이터를 사용한다.

 

그러나 이에 앞서 가장 먼저 DTO(Data Transfer Object) schema를 정의해줘야 한다. DTO란 네트워크를 통해 데이터가 전송되는 방법을 정의한 객체로 타입스크립트의 interface 키워드를 쓰거나 단순히 class로 정의할 수 있다.

 

그러나 클래스로 정의하는 것이 좋은데 그 이유는 타입스크립트 → 자바스크립트 트랜스파일 과정에서도 제거되지 않고 보존되기 때문이다. 즉, 클래스는 ES6 표준 사양이므로 런타임에서도 보존됨을 이용하는 것. 이 특성은 특히 Pipes 에서 중요한데, 변수의 metatype에 접근 가능하게 해주기 때문이다(추후에 배움)

create-cat.dto.ts

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

cats.controller.ts

   
   
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}
HINT
CL를 쓰면 어떤 속성값들만 받을 수 있는지 핸들러에서 필터링할 수 있다. 자세한건 여기를 참고하자.

 

 

Handling errors

Exception filters 챕터 참고

 

 

Librarfy-specific approach

지금까지 response를 처리하는 네스트의 표준 방식을 알아봤다. 물론 @Res() 데코레이터를 쓰면 library-specific response object를 주입해 처리가 가능하긴 하다. 

 

그러나 이 방법은 이전에도 설명했듯 부작용이 많다. 예를 들자면 코드가 clear하지 않고 플랫폼 의존적인 방식이 된다. 뿐만 아니라 테스트하기도 어렵고, 네스트 표준 방식으로 만든 핸들러를 무용지물로 만들어 호환성 문제를 야기하게 된다.  물론 데코레이터의 옵션을 로 설정해주면 어느정도 보완이 되긴 하지만 그닥 추천하는 방법은 아님을 알고 가자.

cats.controller.ts

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