상똥이의 Back-End 공부방
[Nest.js] Controllers 본문
Controllers
컨트롤러는 클라이언트로부터 요청을 받고 응답하는 역할을 맡고 있습니다.
컨트롤러의 목적은 애플리케이션에 들어오는 특정한 요청을 받아들이는 것입니다. 라우팅 매커니즘은 어느 컨트롤러가 어느 요청을 받아들일지 제어합니다. 주로 각각의 컨트롤러는 하나 이상의 경로를 가지며 경로들은 서로 다른 역할을 수행할 수 있습니다.
기본 컨트롤러를 만들기 위해, 우리는 클래스와 데코레이터를 사용합니다. 데코레이터는 클래스에 필요한 메타데이터를 연결하고, 이를 통해 Nest는 라우팅 맵을 생성합니다. (이 라우팅 맵은 들어오는 요청을 해당하는 컨트롤러에 연결합니다.)
HINT
내장된 검증과 함께 CRUD를 빠르게 생성하고 싶다면, CLI의 CRUD 생성 명령어: nest g resource [name] 를 사용할 수 있습니다.
Routing
다음의 예시에서 우리는 기본적인 컨트롤러를 정의하는데 요구되는 @Controller() 데코레이터를 사용할 것입니다. 우리는 상위 경로로 'cats'를 사용할 것입니다. @Controller() 데코레이터 안에 상위 경로를 지정함으로써, 관련된 경로를 그룹화할 수 있으며 코드의 중복을 최소화할 수 있습니다. 예를 들어, 고양이 엔티티와의 상호작용을 관리하는 여러 경로를 /cats 경로로 묶을 수 있습니다. 그 경우, @Controller() 데코레이터에서 경로 상위 경로로 'cats'를 지정하여 파일 내 각 라우트에 대해 경로의 해당 부분을 반복할 필요가 없게 할 수 있습니다.
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 [name] 명령어를 실행하세요.
findAll() 메서드 앞에 오는 @Get() 이라는 HTTP 요청 메서드는 Nest에게 HTTP 요청을 다룰 특정 엔드포인트를 생성하도록 합니다. 그 엔드포인트는 HTTP 요청과 해당 라우트 경로에 대응합니다. 라우트 경로란 무엇일까요? 핸들러의 라우트 경로는 컨트롤러에 (선택적으로) 선언된 상위 경로와 메서드의 데코레이터에 지정된 경로를 결합하여 결정됩니다. 우리가 상위 경로를 'cats'로 지정했고 데코레이터에 다른 어떤 경로에 관한 정보도 기재하지 않았으므로 Nest는 핸들러에 GET /cats 요청을 연결지을 것입니다. 언급한 것처럼, 경로는 컨트롤러의 선택적인 상위 경로 그리고 요청 메서드에 선언된 모든 경로를 포함합니다. 예를 들어 상위 경로인 'cats'와 @Get('breed') 데코레이터가 결합되면 GET /cats/breed 경로를 제공할 것입니다.
위의 예에서, GET 요청이 해당 엔드포인트로 들어오면 Nest는 요청을 사용자가 정의한 findAll() 메서드로 라우팅합니다. 여기서 만든 메서드 이름은 완전히 임의적이라는 것을 명심하세요. 경로를 바인딩하기 위해 메서드를 선언해야 하지만, Nest는 선택된 메서드 이름에 큰 의미를 부여하지 않습니다.
이 메서드는 상태 200을 반환하고 대응되는 응답(위 예시에서는 그저 문자열)을 반환할 것입니다. 왜 이런 일이 발생할까요? 이를 설명하기 위해 Nest가 응답 처리에 사용하는 두 가지 다른 옵션을 소개하겠습니다.
표준 접근 방식 (recommended)
이 내장된 메서드를 사용함으로써 요청 핸들러가 자바스크립트 객체 혹은 배열을 반환할 때 자동으로 JSON형식으로 직렬화됩니다. 자바스크립트의 원시 타입(string, number, boolean)을 반환할 때는 Nest는 직렬화하려는 시도 없이 그저 값만을 반환합니다. 이는 그저 값만을 반환하며 Nest는 다른 일에 집중하여 응답을 더 단순화합니다.
더욱이 상태 코드는 201인 POST 요청을 제외하고는 기본적으로 200입니다. 우리는 이를 @HttpCode(...) 데코레이터를 추가함으로써 쉽게 바꿀 수 있습니다.
Library-specific
library-specific(예: Express) 응답 객체를 사용할 수 있으며, 이는 메서드 핸들러에서 @Res() 데코레이터를 사용하여 주입할 수 있습니다(예: findAll(@Res() response)). 이 방법을 사용하면 해당 객체가 제공하는 네이티브 응답 처리 메서드를 사용할 수 있습니다. 예를 들어, Express에서는 response.status(200).send()와 같은 코드를 사용하여 응답을 생성할 수 있습니다.
WARNING
Nest는 @Res() 또는 @Next()를 사용할 때 library-specific 옵션을 선택한 것으로 판단합니다. 만약 두 접근 모두가 함께 사용된다면 이 경로에 대해 표준 접근 방식이 자동으로 비활성되며 더이상 예상대로 작동하지 않게 됩니다. 두 가지 모두를 동시에 사용하기 위해서(예를 들어, 응답 객체를 주입하여 쿠키/헤더를 설정하되 나머지는 프레임워크에 맡기는 경우)는 @Res() 내부에 passthrough 옵션을 true로 설정해야 합니다. @Res({ passthrough: true})
Request Object
핸들러는 종종 클라이언트 요청 세부정보에 접근할 필요가 있습니다. Nest는 기본적으로 Express를 사용하는 기본 플랫폼의 요청 객체에 접근할 수 있는 기능을 제공합니다. 요청 객체에 접근하려면, 핸들러의 시그니처에 @Req() 데코레이터를 추가하여 Nest가 이를 주입하도록 지시할 수 있습니다.
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 패키지를 설치해주세요.
요청 객체는 HTTP 요청을 나타내며, 요청 쿼리 문자열, 매개변수, HTTP 헤더 및 본문에 대한 속성을 가집니다. 대부분의 경우 이러한 속성을 수동으로 가져올 필요는 없습니다. 대신, @Body() 또는 @Query()와 같은 전용 데코레이터를 사용할 수 있으며, 이러한 데코레이터는 기본적으로 제공됩니다. 아래는 제공되는 데코레이터와 이들이 나타내는 기본 플랫폼별 객체의 목록입니다.
@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 |
Resources
앞서 우리는 cats 자원을 가져오기 위해 엔드포인트를 정의했습니다. (GET 경로) 우리는 새로운 레코드를 생성하는 엔드포인트 또한 제공하고 싶습니다. 이를 위해 POST 핸들러를 생성하겠습니다.
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() 데코레이터를 추가해 사용해 쉽게 바꿀 수 있습니다.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
HINT
@nestjs/common 패키지에서 HttpCode를 가져옵니다
만약 상태 코드가 다양한 원인으로 인해 고정적이지 않을 수 있습니다. 이런 경우엔 library-specific 응답(@Res()를 주입하여)객체를 사용할 수 있습니다. 에러가 발생하는 경우 exception을 사용하세요.
Headers
사용자 정의 응답 헤더를 지정하려면, @Header() 데코레이터를 사용하거나 라이브러리 특정 응답 객체를 사용하여 res.header()를 직접 호출할 수 있습니다.
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
HINT
@nestjs/common 패키지에서 Header를 가져옵니다
Redirection
응답을 특정 URL로 리디렉션하려면, @Redirect() 데코레이터를 사용하거나 라이브러리 특정 응답 객체를 사용하여 res.redirect()를 직접 호출할 수 있습니다.
@Redirect()는 두 개의 인수, url과 statusCode를 취하며, 둘 다 선택적입니다. statusCode의 기본값은 생략할 경우 302 (Found)입니다.
@Get()
@Redirect('https://nestjs.com', 301)
HINT
가끔 HTTP 상태 코드나 리디렉션 URL을 동적으로 결정하고 싶다면, @nestjs/common에서 제공하는 HttpRedirectResponse
인터페이스를 따르는 객체를 반환하여 처리할 수 있습니다.
반환된 값은 @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인 고양이를 가져오는 경우). 매개변수가 있는 라우트를 정의하려면, 경로의 특정 위치에서 동적 값을 캡처하기 위해 경로 매개변수 토큰을 추가할 수 있습니다. 아래의 @Get() 데코레이터 예제에서 라우트 매개변수 토큰 사용법을 보여줍니다. 이렇게 선언된 라우트 매개변수는 @Param() 데코레이터를 사용하여 접근할 수 있으며, 이는 메서드 시그니처에 추가해야 합니다.
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
HINT
파라미터가 포함된 경로는 동적인 경로 다음에 지정되어야 합니다. 이는 매개변수가 있는 경로가 정적 경로를 위한 트래픽을 가로채는 것을 방지합니다.
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Param()은 메서드 매개변수(위의 예제에서 params)의 기능을 추가하는 데 사용되며, 기능이 추가된 메서드 매개변수 내부에서 라우트 매개변수를 해당 매개변수의 속성으로 사용할 수 있게 합니다. 위의 코드에서 보듯이, params.id를 통해 id 매개변수에 접근할 수 있습니다. 또한, 데코레이터에 특정 매개변수 토큰을 전달하면, 메서드 본문에서 직접 라우트 매개변수를 이름으로 참조할 수 있습니다.
HINT
@nestjs/common에서 Param을 가져옵니다
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
Sub-Domain Routing
@Controller 데코레이터는 host 옵션을 받아, 들어오는 요청의 HTTP 호스트가 특정 값과 일치하도록 요구할 수 있습니다.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
WARNING
Fastify는 중첩 라우터를 지원하지 않으므로, 서브도메인 라우팅을 사용할 때는 (기본) Express 어댑터를 대신 사용해야 합니다.
라우트 경로와 비슷하게, 호스트 옵션은 호스트 내에 있는 동적 데이터를 가져오기 위해 토큰을 사용할 수 있습니다. 아래 예시의 @Controller() 데코레이터 내 호스트 파라미터 토큰은 사용 방법을 보여줍니다. 이런 방식으로 선언된 호스트 파라미터는 메서드 시그니처에 추가되어야 하는 @HostParam() 데코레이터를 사용해 접근할 수 있습니다.
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
Scopes
Nest를 배울 예정이 없었던 다른 언어를 사용하던 사람들은 거의 모든 것이 들어오는 요청 간에 공유된다는 사실이 놀라울 수 있습니다. 데이터베이스와 전역적으로 접근 가능한 싱글톤 서비스에 대한 연결 풀이 존재합니다. Node.js가 서버리스 멀티 스레드 형식이 아님을 명심하세요. 이로인해 싱글턴 인스턴스를 사용하는 것은 완전히 안전합니다.
그러나 요청 기반의 컨트롤러 생애 주기가 원하는 동작일 때가 있을 수 있습니다. 예를 들어, GraphQL 애플리케이션에서 요청별 캐싱, 요청 추적 또는 다중 테넌시와 같은 경우입니다.
Asynchronicity
우리는 모던 자바스크립트를 사용하며 데이터 추출 작업이 비동기적임을 알고 있습니다. 이것이 Nest가 async 기능을 지원하고 잘 작동하는 이유입니다.
모든 async 기능은 Promise를 반환해야합니다. 이는 Nest가 직접 해결할 수 있도록 연기된 값을 반환할 수 있음을 뜻합니다. 다음의 예시를 통해 알아보겠습니다.:
@Get()
async findAll(): Promise<any[]> {
return [];
}
위의 코드는 유효합니다. 더욱이 Nest 라우트 핸들러는 obervable streams를 반환할 수 있어 오히려 더 강력합니다. Nest는 자동으로 스트림의 소스를 읽고 스트림이 완료되면 마지막으로 내보낸 값을 가져옵니다.
@Get()
findAll(): Observable<any[]> {
return of([]);
}
위 두 가지 모두 작동하며 선호에 따라 사용할 수 있습니다.
Request payloads
앞선 예시의 POST 라우트 핸들러는 클라이언트 파라미터를 받지 않았습니다. 이를 @Body() 데코레이터를 추가하여 수정해보겠습니다.
우선 (타입스크립트를 사용하는 경우) 우리는 DTO(Data Transfer Object)스키마를 정해야합니다. DTO는 데이터가 네트워크에 어떤 식으로 전달될지 정의하는 객체입니다. TypeScript의 인터페이스 또는 클래스를 사용하여 정의할 수 있습니다. 흥미롭게도, 우리는 클래스를 사용할 것을 권장합니다. 그 이유는 클래스가 ES6 표준이기 때문이고 그로 인해 변환된 자바스크립트 내에서 실제 엔티티로 보존되기 때문입니다. 반면에, TypeScript 인터페이스는 변환 과정에서 제거되기 때문에 Nest는 런타임에서 이를 참조할 수 없습니다. 이는 중요한데, 왜냐하면 Pipes와 같은 기능들은 런타임에서 변수의 메타타입에 접근할 수 있을 때 추가적인 가능성을 제공하기 때문입니다.
CreateCatDto 클래스를 생성하겠습니다.:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
세 가지의 기본 속성을 가집니다. 그에 따라 우리는 CatsController 내부에 새로 생성된 DTO를 사용할 수 있습니다.:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
HINT
ValidationPipe는 메서드 핸들러에 전달되어서는 안 될 속성을 거를 수 있습니다. 이 경우 우리는 허용되는 속성 목록을 명시적으로 만들 수 있으며 이 목록에 포함되지 않는 속성은 만들어질 객체에서 자동으로 제거됩니다. CreateCatDto 예시에서 허가되는 속성 목록에는 name, age 그리고 breed 속성 뿐입니다.
Full resource sample
아래는 기본적인 Controller를 생성할 때 사용 가능한 몇몇의 데코레이터를 사용하는 예시입니다. 이 컨트롤러는 데이터에 접근하고 처리하는 몇 개의 메서드를 보여줍니다.
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는 이 모든 코드를 일일히 작성하는 것을 피하고 개발자의 일을 더욱 평하게 만들어주기 위해 보일러플레이트(모든 코드를 작성하기 위해 항상 필요한 부분)를 자동으로 생성하는 생성 명령어를 제공합니다.
Getting up and running
앞서 Controller를 열심히 작성했지만 Nest에서는 여전히 Controller가 존재하는지 모르고 결과적으로 이 클래스의 인스턴스를 생성하지 않습니다.
컨트롤러는 항상 모듈에 속하므로, @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의 표준 방식으로 응답을 조작하는 방법에 대해 설명했습니다. 응답을 조작하는 두 번째 방법은 라이브러리 특정 응답 객체를 사용하는 것입니다. 특정 응답 객체를 주입하려면 @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([]);
}
}
이 방법은 작동하며, 응답 객체에 대한 완전한 제어(헤더 조작, 라이브러리 특화 기능 등)를 제공함으로써 일부 측면에서 더 유연성을 제공하기도 하지만, 주의해서 사용해야 합니다. 일반적으로 이 접근 방식은 훨씬 덜 명확하며 몇 가지 단점이 있습니다. 주요 단점은 코드가 플랫폼에 종속된다는 점입니다(기반 라이브러리에 따라 응답 객체의 API가 다를 수 있음). 또한 테스트하기가 어려워집니다(응답 객체를 모의(mock)해야 함 등).
또한, 위의 예제에서는 Interceptor나 @HttpCode(), @Header() 데코레이터와 같이 Nest 표준 응답 처리에 의존하는 Nest 기능과의 호환성을 잃게 됩니다. 이를 해결하기 위해서는 passthrough 옵션을 true로 설정하면 됩니다.
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.status(HttpStatus.OK);
return [];
}
이제 네이티브 응답 객체와 상호작용할 수 있습니다(예: 특정 조건에 따라 쿠키나 헤더 설정). 그러나 나머지 부분은 프레임워크에 맡길 수 있습니다.
'Nest.JS > Docs' 카테고리의 다른 글
[Nest.js] Middleware (0) | 2024.09.05 |
---|---|
[Nest.js] Modules (1) | 2024.09.04 |
[Nest.js] Providers (0) | 2024.09.04 |
[Nest.js] First steps (0) | 2024.03.19 |
[Nest.js] Introduction (1) | 2024.03.19 |