[Nestjs] 인터페이스 - 컨트롤러

The Dragonite·2023년 11월 19일
0

Nestjs

목록 보기
4/9
post-thumbnail

Controller

Nest의 컨트롤러는 MVC 패턴에서 이야기하는 그 컨트롤러를 이야기한다.

컨트롤러는 클라이언트에서 요청(request)를 받고 응답(response)를 돌려주는 인터페이스 역할을 한다.


여기에서 엔트포인트 라우팅을 통해 각 컨트롤러가 요청을 받을 수 있도록 요청을 분류하는데, 여기서 컨트롤러를 사용 목적에 따라 구분하여 작성하는 것이 구조적이고 모듈화 된 소프트웨어를 작성하는데 도움 준다.


구성 방법

우선 Nestjs는 명령어를 통해 여러 보일러 플레이트를 자동으로 생성할 수 있도록 해주는데 사용 방법은 크게 두가지가 있다.

$ nest g resource [name]

을 사용하는 경우 CRUD 보일러 플레이트를 자동으로 생성해주는데, 그외에도 다른 형태의 보일러 플레이트 코드를 지원한다.

위의 명령어를 사용할 경우 아래와 같이 자동으로 생성된다.

$ nest g controller [name]

이 명령어를 사용할 경우 컨트롤러만 생성해주는데 사용 목적에 따라 나눠서 사용하는 것이 좋을 것 같다

nest 명령어로 생성할 경우 자동으로 설정되는 것들이 많아 프로젝트를 만드는데 수월하다...

Routing

우선 보일러 플레이트를 깔았을 경우 처음 깔리는 소스코드를 살펴보겠다.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
	constructor(private readonly appService: AppService) {};
  
	@Get()
  	getHello(): string {
    	return this.appService.getHello();
    }
}

위의 코드를 보면 데커레이터를 적극 활용하여 애플리케이션의 핵심 로직에 집중할 수 있도록 도움을 받고 있는 것을 알 수 있다.

추가로 설명을 하자면 @Controller 데커레이터를 클래스에 선언하는 것으로 해당 클래스가 컨트롤러의 역할을 하게 만드는 것이다.

GET 요청에 대한 것도 @Get 데커레이터를 사용하여 처리 할 수 있는데, 라우팅 경로는 @Get 데커레이터의 인수로 관리할 수 있다.

(예시 코드)
@Get() //루트경로로 들어오는 요청 처리
@Get('/myroute')//http://localhost:3000/myroute로 들어오는 요청 처리

@Controller 데커레이터에도 인수를 전달 할 수 있는데, 이를 통해 라우팅 경로의 prefix를 지정하는 것이다.

(예시 코드)
@Controller('app') // http://localhost:3000/app/myroute

Request Object

요청 객체

클라이언트가 요청을 보내면서 서버가 원하는 정보를 함께 전송한다.

Nest는 요청과 함께 전달되는 데이터를 핸들러가 다룰 수 있는 객체로 변환한다. 이렇게 변환된 객체는 @Req 데커레이터를 사용하여 활용할 수 있다.

(예시 코드)
import { Get, Req } from '@nestjs/common';

...

@Get()
getHello(@Req() req:Request): string {
	console.log(req);
  	return this.appService.getHello();
}

요청 객체는 다음과 같은 것들로 구성이 되어있다.

Received HTTP request:
Method: GET
URL: /
Headers: {
  host: 'localhost:3000',
  connection: 'keep-alive',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  ...
}
------------------------

우리가 API를 작성할 때는 요청 객체를 직접 다룰 경우가 드물다. Nest는 @Query( ), @Param(key?: string), @Body( ) 데커레이터를 사용하여 요청에 포함된 쿼리 매개변수, 경로 변수, 본문을 쉽게 받을 수 있다.


Response

응답

CRUD API로 구성한 서버에서 특정 리소스에 대한 CRUD 요청 결과는 다음과 같다.

응답 상태 코드

POST : 201

GET : 200

PATCH : 200

DELETE : 200

각 요청의 응답 본문은 스트링 값을 가지고 있는데 이는 컨트롤러의 각 메서드가 리턴하는 값이라고 한다.

이렇게 응답을 어떤 방식으로 처리 할지 Nestjs는 미리 정의해두었는데, javascript primitive type을 리턴할 경우에 직렬화없이 보내지만, 객체를 리턴한다면 직렬화(serialization, JSON.stringify( )?)를 통해 JSON으로 자동으로 변환해준다.

Express를 사용할 경우에 @Res 데커레이터를 이용해서 Express 응답 객체를 다룰 수 있다고 한다.

CRUD에 대한 성공 응답으로 POST는 201, 그 외는 200을 보내는데 이것을 사용자화 하고 싶다면 @HttpCode 데커레이터를 사용하여 마음대로 바꾸어 사용할 수 있다.

그래도 공식문서에서 규정하는 코드를 사용하는 것이 좋아 보인다.

참조 링크

요청을 처리하는 중 에러가 발생하거나 예외를 던져야 한다면 다음과 같이 처리 할 수 있다.

@Get(':id')
findOne(@Param('id') id: string) {
	if (+id < 1) { //string 인 id를 +id 를 사용해 자동으로 number 형변환을 시킨다.
    	throw new BadRequestException('id has to be bigger than 0');
    }
  	return this.usersService.findOne(+id);
}

이에 대해서 다음과 같이 requst를 던지면 아래와 같은 결과가 나온다

$ curl -X GET http://localhost:3000/users/0
{
	"statusCode": 400,
    "message": "id has to be bigger than 0",
    "error": "Bad Request"
}

Header

헤더

응답에 커스텀 헤더를 추가하고 싶다면 @Header 데커레이터를 사용하면 되는데, 그 인수로는 헤더 이름과 값을 받는다.

그 전에 커스텀 헤더들은 아래와 같은 경우에 주로 사용된다.

  • 인증 및 권한 부여
  • 캐싱 및 버전 관리
  • 로그 및 분석
  • 사용자 정의

더 자세한 것들은 추후에 다루도록 하겠다.


Redirection

리디렉션

서버가 요청을 처리 한 후에 요청을 보낸 클라이언트를 특정 페이지로 이동하고 싶은 경우에는 리디렉션 기능을 사용 하면된다.

응답 본문에 리디렉션 할 URL을 넘겨주는 방법도 가능하지만 Nestjs에서는 @Redirect 데커레이터를 사용하는 편이 더 간편하다. @Redirect 데커레이터는 첫번째 인수로 리디렉션 할 URL을 받고 두번째 인수로 상태 코드를 받는다. 예시는 아래와 같다.

import { Redirect } from '@nestjs/common';

@Redirect('https://nestjs.com', 301)
@Get(':id')
findOne(@Param('id') id: string) {
	return this.usersService.findOne(+id);
}

요청 결과에 따라서 동적으로 리디렉트하고자 하면 응답을 다음과 같은 형식의 객체로 리턴하면 된다.

{
	"url": string,
    "statusCode": number
}

예시 코드는 다음과 같다.

@Get('redirect/docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
	if( version && version === '5' ) { //https://localhost:3000/redirect/docs?version=5
    	return { url: 'https://docs.nestjs.com/v5' };
    }
}

Nest는 자바스크립트 객체를 리턴하면 자동으로 JSON.stringify( )를 적용해서 보내주므로 위와 같이 코드 작성이 가능하다.

그렇다면 "statusCode"에 해당하는 부분은 굳이 안써도 되는건가...?

Route Parameter

라우트 매개변수

라우트 매개변수는 패스 매개변수라고도 한다. 예를 들면 http://localhost:3000/users/1의 부분에서 1의 부분이 유저의 ID인데 이는 동적으로 구성되고 경로를 구성하는 매개변수가 된다.

전달 받은 매개변수는 함수의 인수에서 @Param 데커레이터로 주입받을 수 있다.

매개변수를 받는 방법은 총 2가지가 있는데, 하나는 매개변수를 여러개를 받을 경우 객체로 한 번에 받는 방법이고, 다른 하나는 매개변수를 따로 받는 것이다. 아래는 동일한 도메인에 대한 각각의 예시코드이다.

//매개변수를 동시에 받아 객체로 다루는 방법
@Get(':userId/memo/:memoId')
getUserMemo(@Param() params: {[key: string]: string}) {
	return `userId: ${params.userId}, memoId: ${params.memoId}`;
}

/*********************************/
//매개변수를 각각 따로 받아 각각의 타입을 지정해주는 방법
@Get(':userId/memo/:memoId')
getUserMemo(
	@Param('userId') userId: string,
  	@Param('memoId') memoId: string,
) {
	return `userId: ${params.userId}, memoId: ${params.memoId}`;
}

위의 코드를 보면 알겠지만 첫번째의 방식은 params의 타입이 any가 되어 타입스크립트가 권장하지 않는 방법이므로 두번째를 사용하는 것이 좋아보인다. 또한 두번째 방법의 경우 REST API를 구성할 때, 라우팅 매개변수가 너무 많아지는 것을 지양하므로 @Param 데커레이터로 받을 정보가 적으므로 코드가 길어지는 것을 걱정할 필요는 없다.


Domain Routing

하위 도메인 라우팅

현재 프로젝트의 도메인이 example.com이고, API 요청은 api.example.com으로 받는다고 할 때, 두 다른 도메인으로 들어온 요청을 서로 다르게 처리하고 싶다고 할 때, 이런 식으로 하위 도메인 라웉팅 기법을 쓸 수 있다.

우선 새로운 API컨트롤러를 생성하고, ApiController에서 같ㅌ은 엔드포인트를 받을 수 있도록 ApiController가 먼저 처리되도록 AppModule에서 수정을 해준다.

nest g co Api
@Module({
	controllers: [ApiController, AppController],
  	...
})

@Controller 데커레이터는 ControllerOptions 객체를 인수로 받는데, host 속성으로 하위 도메인을 기술 하면된다.

@Controller({ host: 'api.example.com' })
export class ApiController {
	@Get()
  	index(): string {
    	return 'Hello, Api';
    }
}

로컬에서 테스트 할 때는 하위 도메인에 대한 curl 명령어가 제대로 작동하도록 api.localhost가 로컬 요청을 받을 수 있도록 설정을 해줘야한다. 이를 해결하기 위해서는 /etc/hosts 파일의 마지막에 127.0.0.1 api.localhost와 같은 방식으로 추가한 후에 서버를 재구동 해야한다.

앞서 @Param 데커레이터를 배웠으니 이를 활용하여 서브 도메인을 동적으로 받을 수 있도록 할 수 있도록 해보자.

서브 도메인의 경우에는 @Param 데커레이터가 아닌 @HostParam 데커레이터를 사용하는데, 이를 사용 하여 API 버저닝을 할 수 있다.

@Controller({ host: ':version.api.localhost' })
export class ApiController {
	@Get()
  	index(@HostParam('version') version: string): string {
    	return `Hello, Api ${version}`;	
    }
}
$ curl http://v1.api.localhost:3000
Hello, Api v1
$ curl http://api.localhost:3000
Hello World!

host param없이 host로 요청하면 기존의 도메인으로 요청처리 되는 것을 알 수 있다.

그렇다면, http://localhost:3000 과 http://api.localhost:3000을 @HostParam 데커레이터를 활용하여 나타낼 수 있지 않은가 하는 의문점이 들 수도 있다.

보통의 경우 이러한 경우에 @HostParam을 사용한다고 한다.

  1. 다중 도메인 라우팅: 서비스에 여러 도메인이나 서브도메인이 연결되어 있고, 이에 따라 다른 로직이나 데이터를 처리해야 하는 경우에 유용합니다. @HostParam을 사용하여 현재 요청의 호스트를 동적으로 가져와서 처리할 수 있습니다.

  2. 환경별 호스트 설정: 개발, 테스트, 프로덕션 환경 등에서 다른 호스트를 사용해야 하는 경우, @HostParam을 사용하여 각각의 환경에 따라 동적으로 호스트를 설정할 수 있습니다.

  3. 서브도메인 기반 라우팅: 특정 서비스 또는 기능을 서브도메인으로 분리하여 처리할 때, @HostParam을 사용하여 현재 요청의 서브도메인을 동적으로 가져와 라우팅할 수 있습니다.

  4. 다중 API 버전 관리: 위에서 언급한 것처럼 여러 API 버전을 관리할 때, 각 API 버전에 대한 서브도메인을 사용하여 @HostParam을 통해 동적으로 버전을 선택할 수 있습니다.


Payload Handling

페이로드 다루기

POST, PUT, PATCH 요청은 보통 처리에 필요한 데이터를 함께 실어 보낸다. 우리는 이 데이터 덩어리를 body, payload라고 한다. Nestjs에서는 DTO(data transfer object, 데이터 전송 객체)가 구현되어 있어 본문을 쉽게 다룰 수 있다.

POST/users로 들어오는 본문을 CreateUserDto로 받는 것을 구현해보자.

export class CreateUserDto {
  name: string;
  email: string;
}

/***/
@Post()
create(@Body() createUserDto: CreateUserDto) {
  const { name, email } = createUserDto;
  
  return `Created User. name: ${name}, email: ${email}`;
}

위의 코드에 아래와 같이 API를 요청해 결과를 확인 해보자

$ curl -X POST http://localhost:3000/users -H "Content-type: application/json" 
  -d '{"name": "toragonite", "email": "YOUR_EMAIL@gmail.com"}'
Created User. name: toragonite, email: YOUR_EMAIL@gmail.com

url 쿼리문으로 주는 옵션돋 DTO로 묶어서 처리를 할 수 있는데 사용법은 다음과 같다.

// GET /users?offset=0&limit=10으로 요청을 할 경우
export class GetUserDto {
  offset: number;
  limit: number;
}

...


@Get()
create(@Query getUsersDto: GetUsetsDto) {...}

이 포스트는 위 책을 기반으로 작성하고 있습니다...

0개의 댓글

관련 채용 정보