NEST.JS - OverView: Controllers

rkdden12·2021년 6월 10일
0

Nest.js

목록 보기
3/4

Controllers (컨트롤러)

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

힌트
validation이 내장된 CRUD 컨트롤러를 빠르게 생성하려면 CLI의 CRUD 생성기: nest g resource [name]을 사용할 수 있다.


Routing (라우팅)

  • 아래 예제에서는 기본 컨트롤러를 정의하는 데 필수인 @Controller() 데코레이터를 사용한다.
  • 선택적 라우트 경로(path) 접두사 cats를 지정한다. (cats가 아닌 다른것이여도 상관없다.)
  • @Controller() 데코레이터에서 경로(path) 접두사를 사용하면 관련 라우트 집합을 쉽게 그룹화하고 반복코드를 최소화할 수 있다.
  • 예를 들어 /customers 라우트 아래에서 고객 엔터티와의 상호작용을 관리하는 라우트 집합을 그룹화하도록 선택할 수 있다.
  • 이 경우 @Controller() 데코레이터에서 경로(path) 접두사 customers를 지정하여 파일의 각 라우트에 대해 경로(path)의 해당부분을 반복할 필요가 없다.
// cats.controller.ts
import { Controller, Get } from '@nestjs/common';

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

힌트
CLI를 사용하여 컨트롤러를 만들려면 $ nest g controller 컨트롤러이름 명령을 실행하면 된다.

  • findAll() 메서드 앞에 있는 @Get() HTTP 요청 메서드 데코레이터는 Nest에 HTTP 요청에 대한 특정 엔드포인트에 대한 핸들러를 생성하도록 지시한다.
  • 엔드포인트는 HTTP 요청 메서드(이 경우 GET) 및 라우트 경로에 해당한다.
    • 쉽게말해서 @Get 데코레이터는 localhost:3000/cats에 대한 get 요청을 수행한다.
  • 핸들러의 라우트 경로는 컨트롤러에 대해 선언된(선택 사항) 접두사와 요청 데코레이터에 지정된 경로를 연결하여 결정된다.
  • 모든 라우트 (cats)에 대한 접두사를 선언하고 데코레이터에 경로 정보를 추가하지 않았으므로 Nest는 GET /cats 요청을 이 핸들러에 매핑한다.
  • 경로에는 선택적 컨트롤러 경로 접두사 및 요청 메서드 데코레이터에서 선언된 모든 경로 문자열이 모두 포함된다.
  • 예를 들어, 데코레이터 @Get('profile')과 결합된 customers의 경로 접두사는 GET /customers/profile과 같은 요청에 대한 라우트 매핑을 생성한다.
  • 위의 예제에서 이 엔드포인트에 GET 요청이 있을 때 Nest는 요청을 커스텀 findAll() 메서드로 라우팅한다.
  • 여기서 선택한 메서드 이름은 바뀌어도 상관없다.
  • 분명히 경로를 바인딩할 메서드를 선언해야 하지만 Nest는 선택한 메서드 이름에 어떤 의미도 부여하지 않는다.
    • 당연하지만 한 요청에서 return은 한번만 쓰여야 한다.
  • 이 메서드는 200 상태 코드와 관련 응답을 반환한다.
  • 위의 경우에는 문자열일 뿐이다.
  • 이유를 설명하기 위해 먼저 Nest가 응답을 조작하기 위해 다른 두가지 옵션을 사용한다는 개념을 알아보자.
    • 표준 (권장)
      • 이 내장 메서드를 사용하면 요청 핸들러가 자바스크립트 객체 또는 배열을 반환할 때 자동으로 JSON으로 직렬화된다.
      • 그러나 자바스크립트 기본 타입(예: string, number, boolean)을 반환하면 Nest는 직렬화를 시도하지 않고 값만 보낸다.
      • 이렇게 하면 응답처리가 간단해진다.
      • 값을 반환하기만 하면 Nest가 나머지 작업을 처리한다.
      • 또한 응답의 상태 코드는 201을 사용하는 POST 요청을 제외하고는 항상 기본적으로 200이다.
      • 핸들러 수준에서 @HttpCode(...) 데코레이터를 추가하여 이 동작을 쉽게 변경할 수 있습니다(자세한 내용은 아래 상태 코드(status code)를 참고하자).
    • 라이브별 Library-specific
      • 메소드 핸들러 시그니처(예: findAll(@Res() response))에서 @Res() 데코레이터를 사용하여 삽입할 수 있는 라이브러리별(예: Express) 응답객체를 사용할 수 있다.
      • 예를 들어 Express에서는 response.status(200).send()와 같은 코드를 사용하여 응답을 구성할 수 있다.

경고

  • Nest는 핸들러가 @Res() 또는 @Next()를 사용할 때 이를 감지하여 라이브러리별 옵션을 선택했음을 나타낸다.
  • 두 접근 방식을 동시에 사용하는 경우 이 단일 라우트에 대해 표준 접근방식이 자동으로 비활성화되고 더 이상 예상대로 작동하지 않는다.
  • 두 접근 방식을 동시에 사용하려면 (예: 쿠키/헤더만 설정하고 나머지는 프레임워크에 남겨 두도록 응답객체를 삽입) @Res({ passthrough: true }) 데코레이터에서 passthrough 옵션을 true로 설정해야 한다.

Request object (Requst 객체)

  • 핸들러는 종종 클라이언트 요청 세부정보에 액세스해야 한다.
  • Nest는 기본 플랫폼(기본적으로 Express)의 요청객체에 대한 액세스를 제공한다.
  • 핸들러의 시그니처에 @Req() 데코레이터를 추가하여 Nest에 주입하도록 지시하여 요청객체에 액세스할 수 있다.
// 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';
  }
}
  • 힌트
    • express 타입(위의 request: Request 매개변수 예제 참조)을 활용하려면 @types/express 패키지를 설치합니다.
  • 요청객체는 HTTP 요청을 나타내며 요청 쿼리 문자열, 매개변수, HTTP 헤더 및 본문에 대한 속성을 포함한다(자세한 내용은 여기 참조).
  • 대부분의 경우 이러한 속성을 수동으로 가져올 필요가 없다.
  • 바로 사용할 수 있는 @Body() 또는 @Query()와 같은 전용 데코레이터를 대신 사용할 수 있다.
  • 아래는 제공된 데코레이터와 이들이 나타내는 일반 플랫폼별 객체 목록이다.
Nest에서는?			|		Express에서는?

@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
  • 기본 HTTP 플랫폼(예: Express 및 Fastify)에서 입력과의 호환성을 위해 Nest는 @Res() 및 @Response() 데코레이터를 제공한다.
  • @Res()는 단순히 @Response()의 별칭이다.
  • @Res(), @Response() 둘 다 기본 네이티브 플랫폼 response 객체 인터페이스를 직접 노출한다.
  • 이를 사용하는 경우 기본 라이브러리(예: @types/express)에 대한 타입도 가져와야 최대한 활용할 수 있다.
  • 메소드 핸들러에 @Res() 또는 @Response()를 삽입할 때 해당 핸들러에 대해 Nest를 라이브러리별 모드로 설정하고 응답을 관리해야 한다.
  • 그렇게 할 때 응답객체(예: res.json(...) 또는 res.send(...))를 호출하여 일종의 응답을 발행해야한다.
  • 그렇지 않으면 HTTP 서버가 중단된다.

Resources (리소스)

  • 이전에는 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는 모든 표준 HTTP 메소드에 대한 데코레이터를 제공한다.
    • @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options() 및 @Head(). 또한 @All()은 이들 모두를 처리하는 엔드 포인트를 정의한다.

Route wildcards(라우트 와일드 카드)

  • 패턴 기반 라우트도 지원된다.
  • 예를 들어 별표( * )는 와일드카드로 사용되며 모든 문자조합과 일치한다.
@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}
  • 'ab*cd' 라우트 경로는 abcd, ab_cd, abecd 등과 일치한다.
  • ?, +, * 및 () 문자는 라우트 경로에 사용될 수 있으며 해당 정규표현식 대응 부분의 하위집합이다.
  • 하이픈(-)과 점(.)은 문자열 기반 경로로 문자 그대로 해석된다.

Status code(상태코드)

  • 201인 POST 요청을 제외하고 응답 상태코드는 기본적으로 항상 200이다.
  • 핸들러 레벨에서 @HttpCode(...) 데코레이터를 추가하여 이 동작을 쉽게 변경할 수 있다.
  • @nestjs/common 패키지에서 HttpCode를 가져온다.
@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}
  • 종종 상태코드는 정적이 아니지만 다양한 요인에 따라 달라진다.
  • 이 경우 라이브러리별 응답(@Res()를 사용하여 주입)객체를 사용할 수 있다. (또는 오류가 발생한 경우 예외 발생).

Headers (헤더)

  • 커스텀 응답헤더를 지정하려면 @Header() 데코레이터 또는 라이브러리별 응답객체를 사용할 수 있다. (그리고 res.header()를 직접 호출).
  • @nestjs/common 패키지에서 Header를 가져온다.
@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Redirection (리다이렉션)

  • 응답을 특정 URL로 리다이렉션하려면 @Redirect() 데코레이터 또는 라이브러리별 응답객체(res.redirect()를 직접 호출)를 사용할 수 있다.
  • @Redirect()는 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/' };
  }
}

Route parameters (라우트 매개변수)

  • 요청의 일부로 동적 데이터를 수락해야 하는 경우 정적 경로가 있는 라우트가 작동하지 않는다. (예: GET /cats/1는 ID가 1인 cat을 가져온다).
  • 매개변수가 있는 라우트를 정의하기 위해 라우트 경로에 라우트 매개변수 토큰 tokens을 추가하여 요청 URL의 해당 위치에서 동적값을 캡처할 수 있다.
  • 아래 @Get() 데코레이터 예제의 경로 매개변수 토큰은 이 사용법을 보여준다.
  • 이렇게 선언된 라우트 매개변수는 @Param () 데코레이터를 사용하여 액세스할 수 있으며, 이는 메소드 서명에 추가되어야 한다.

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
  • @Param()은 메서드 매개변수(위의 예에서는 params)를 장식하는데 사용되며, 라우트 매개변수를 메서드 본문내에서 장식된 메서드 매개변수의 속성으로 사용할 수 있도록 한다.
  • 위 코드에서 볼 수 있듯이 params.id를 참조하여 id 매개변수에 액세스할 수 있다.
  • 특정 매개변수 토큰을 데코레이터에 전달한 다음 메서드 본문에서 이름으로 직접 라우트 매개변수를 참조할 수도 있다.
  • 아래 예제를 보자.
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing

  • 죽어도 모르겠다 나중에 다시 찾아보고 추가하자
  • @Controller 데코레이터는 들어오는 요청의 HTTP 호스트가 특정값과 일치하도록 host 옵션을 사용할 수 있다.
@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}
  • 라우트 path와 유사하게 host 옵션은 토큰을 사용하여 호스트 이름의 해당 위치에서 동적값을 캡처할 수 있다.
  • 아래 @Controller() 데코레이터 예제의 호스트 매개변수 토큰은 이 사용법을 보여준다.
  • 이런 방식으로 선언된 호스트 매개변수는 @HostParam() 데코레이터를 사용하여 액세스할 수 있으며 메소드 서명에 추가해야 한다.
@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

Scopes

  • 다른 프로그래밍 언어 배경을 가진 사람들의 경우 Nest에서 거의 모든 것이 들어오는 요청에서 공유된다는 사실을 배우는 것은 예상치 못한 일이다.
  • 데이터베이스에 대한 연결 풀, 전역상태의 싱글톤 서비스 등이 있다.
  • Node.js는 모든 요청이 별도의 스레드에서 처리되는 요청/응답 다중스레드 상태 비저장(Multi-Threaded Stateless) 모델을 따르지 않는다.
  • 따라서 싱글톤 인스턴스를 사용하는 것은 애플리케이션에 완전히 안전하다.
  • 그러나 컨트롤러의 요청 기반 수명이 바람직한 동작일 수 있는 경우가 있다.
  • 예를 들어 GraphQL 애플리케이션의 요청별 캐싱, 요청 추적 또는 멀티테넌시가 있다.
  • 를 제어하는 방법은 나중에 알아본다.

Asynchronicity (비동기)

  • Nest에서는 비동기(async) 기능을 지원하고 잘 작동한다.
  • 모든 비동기 함수는 Promise를 반환해야 한다.
  • 즉, Nest가 자체적으로 해결할 수 있는 지연된 값을 반환할 수 있다.
  • 아래의 예제를 살펴보자
// cats.controller.ts
@Get()
async findAll(): Promise<any[]> {
  return [];
}
  • 위의 코드는 유효한 코드이다.
  • Nest 라우트 핸들러는 RxJS 관찰가능한 스트림를 반환할 수 있으므로 훨씬 더 강력하다.
  • Nest는 자동으로 아래의 소스를 구독하고 스트림이 완료되면 마지막으로 내보낸 값을 가져온다.
// cats.controller.ts
@Get()
findAll(): Observable<any[]> {
  return of([]);
}
  • 위의 두가지 접근방식이 모두 작동하며 요구사항에 맞는 것을 사용할 수 있다.

Request payloads

  • POST 라우트 핸들러의 이전 예제는 클라이언트 매개변수를 허용하지 않았다.
  • 여기에 @Body() 데코레이터를 추가하여 이 문제를 해결해보자
  • 하지만 먼저(TypeScript를 사용하는 경우) DTO(데이터 전송 개체) 스키마를 결정해야 한다.
  • DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체이다.
  • TypeScript 인터페이스를 사용하거나 간단한 클래스를 사용하여 DTO 스키마를 확인할 수 있다.
  • 여기에서는 클래스을 사용하는 것이 좋다.
  • 이유는 클래스는 자바스크립트 ES6 표준의 일부이므로 컴파일된 자바스크립트에서 실제 엔티티로 유지된다.
  • 반면에 TypeScript 인터페이스는 트랜스 파일중에 제거되기 때문에 Nest는 런타임에 이를 참조할 수 없다.
  • 파이프와 같은 기능은 런타임에 변수의 메타타입에 액세스할 수 있을 때 추가 가능성을 제공하기 때문에 중요하다.
  • 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';
}

Full resource sample (전체 코드 예제)

  • 아래는 사용 가능한 여러 데코레이터를 사용하여 기본 컨트롤러를 만드는 예제이다.
  • 이 컨트롤러는 내부 데이터에 액세스하고 조작하는 몇가지 방법을 제공한다.
// 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`;
  }
}

Getting up and running

  • 위의 컨트롤러가 완전히 정의된 상태에서도 Nest는 여전히 CatsController가 존재하는지 알지 못하므로 결과적으로 이 클래스의 인스턴스를 만들지 않는다.
  • 컨트롤러는 항상 모듈에 속하므로 @Module() 데코레이터 내에 controllers 배열을 포함한다.
  • 루트 AppModule을 제외한 다른 모듈을 아직 정의하지 않았으므로 이를 사용하여 CatsController를 소개한다.
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}
  • @Module() 데코레이터를 사용하여 모듈 클래스에 메타데이터를 첨부했으며 Nest는 이제 어떤 컨트롤러를 마운트해야 하는지 쉽게 반영할 수 있다.

Library-specific approach

  • 지금까지 응답을 조작하는 Nest 표준방식에 대해 논의했다.
  • 응답을 조작하는 두번째 방법은 라이브러리별 응답객체를 사용하는 것이다.(이 경우 express)
  • 특정 응답객체를 삽입하려면 @Res() 데코레이터를 사용해야 한다.
  • 차이점 확인을 위해 CatsController를 다음과 같이 다시 작성해 보자.
// 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([]);
  }
}
  • 이 접근 방식이 작동하고 실제로 응답객체(헤더 조작, 라이브러리별 기능등)에 대한 전체제어를 제공함으로써 어떤 방식으로든 더 많은 유연성을 허용하지만 주의해서 사용해야 한다.
  • 일반적으로 접근방식은 덜 명확하고 몇가지 단점이 있다.
  • 가장 큰 단점은 코드가 플랫폼에 종속되고(기본 라이브러리가 응답객체에 대해 다른 API를 가질 수 있기 때문에) 테스트하기가 더 어렵다는 것이다(응답객체를 모의 처리해야하는 등).
  • 또한 위의 예에서 인터셉터 및 @HttpCode()/@Header() 데코레이터와 같은 Nest 표준 응답처리에 의존하는 Nest 기능과의 호환성이 손실된다.
  • 이 문제를 해결하려면 다음과 같이 passthrough 옵션을 true로 설정할 수 있다.
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}
  • 이제 네이티브 응답객체와 상호작용할 수 있지만(예: 특정조건에 따라 쿠키 또는 헤더설정) 나머지는 프레임워크에 맡긴다.

출처

profile
Backend Developer

0개의 댓글