본문 바로가기

node js/NestJs

[Nest Js] Nest Js 공식 문서 파헤치기 - Controller(컨트롤러)

트리스티가 Nest Js를 공부하며 남긴 기록입니다. 틀린 내용은 언제든지 말씀해주세요 ~!

 


 

 

📣 Controller란 무엇인가?

 

Controller란 사용자(client)의 요청(request)을 처리하고, 응답(response)을 반환하는 역할을 담당합니다. controller가 없어도 웹 서버를 구축할 수 있지만, 없다면 엄청나게 긴 코드를 읽고 유지 보수해야 하는 불편함이 생깁니다. 따라서 특정 기준에 따라 해당 코드들을 나눠서 제어자 역할을 하는 controller라고 이름 지어서 관리하는 것이죠. controller는 디자인 패턴 중 하나인 MVC 패턴에서 자주 사용되는 개념입니다. 

 

 

 

  1. Model - 데이터와 관련된 작업들을 처리합니다. 데이터베이스에 접근하여 수정, 삭제, 생성 등의 작업이 이뤄집니다.
  2. View - 사용자에게 보여줄 방식을 정의합니다. 
  3. Controller - 사용자의 요청을 처리하고, 그에 관한 응답을 반환합니다. Model에서 처리된 데이터를 View로 전달해주는 중간 매개체 입니다.

 

 

클라이언트에서는 각각의 controller에 존재하는 경로에 접근할 수 있고, controller는 해당 요청에 맞는 응답을 전송합니다. (출처: https://docs.nestjs.kr/controllers)

 

 

 

이 포스팅은 MVC 패턴을 설명하는 포스팅이 아니기 때문에 이 정도로만 설명하고 다시 controller로 넘어가겠습니다.

지난 포스팅에서 저희는 Nest Js에서 특정 클래스를 controller로 만들기 위해서는 @Controller() 데코레이터를 사용해야 한다는 것을 확인했습니다. 

 

@Controller()를 사용해야 해당 클래스가 controller가 됩니다.

 

 

📣 Routing

 

 

controller는 어플리케이션에 대한 특정 요청을 수신하는데, 프로그래머가 각 컨트롤러에서 어떤 요청을 수신할지를 정해줘야 합니다. Nest js의 @Controller() 데코레이터는 이러한 역할을 수행하기 위해 경로를 지정할 수 있는데요. 이것을 라우팅이라고 합니다.

 

적용하는 방법은 간단합니다. 아래처럼 @Controller() 데코레이터 안에 원하는 경로명을 작성해주면 끝납니다. 경로명은 정말 아무거나 작성해도 되지만, 완성도 있는 어플리케이션을 만들기 위해서는 경로명을 통일시켜주는 것이 좋겠죠? 지난 포스팅에서 Nest cli를 통해 controller를 만들었는데, 이때 자동으로 'study'라는 경로가 만들어졌습니다.

 

AppController와는 다르게 StudyController에는 'study'라는 라우팅이 적용되어 있습니다.

 

 

StudyController에 아무것도 없으면 허전하니까 간단하게 @Get() 데코레이터를 사용해서 엔드포인트를 만들어주도록 하겠습니다. 

 

 

 

 

코드를 작성하고 localhost:3000으로 접속하면 다음과 같이 Hello World! 가 출력된 것을 확인할 수 있습니다. app.controller.ts의 @Controller() 데코레이터에는 저희가 어떠한 경로도 지정해주지 않았는데, 이 경우 해당 컨트롤러가 디폴트 경로가 되는 것을 알 수 있습니다. 그래서 Hello World! 가 출력된 것이죠.

 

@Controller() 데코레이터에 어떠한 경로도 설정하지 않으면, 해당 컨트롤러가 디폴트가 된다.

 

 

 

그렇다면 방금 작성한 hello Tristy! 를 보기 위해서는 어떻게 해야 할까요? StudyController의 경우에는 @Controller() 데코레이터의 경로를 'study'로 작성했으니 당연히 study로 접근해야겠죠.

따라서 localhost:3000이 아니라 localhost:3000/study로 접근해줘야 합니다. 이렇게 접근하면 그림처럼 잘 출력되는 것을 확인할 수 있습니다. 이처럼 @Controller() 데코레이터에 경로를 지정하면 사용자가 해당 경로로 접근할 수 있습니다.

 

 

@Controller() 데코레이터에 특정 경로를 설정하면, 해당 경로로 접근할 수 있다.

 

Nest에서는 @Controller() 데코레이터 말고도 @Get(), @Post(), @Delete() 등의 HTTP 요청 메소드 데코레이터를 사용해서 HTTP 요청에 대한 특정 엔트 포인트를 설정할 수 있습니다. 예제로 @Get() 데코레이터에 라우팅을 설정해 보겠습니다. @Get() 데코레이터에 'Tristy'라는 경로를 설정해보겠습니다. 

 

 

 

이렇게 설정을 하면, localhost:3000/study/Tristy라는 경로로 접근해야 원하는 문자열을 볼 수 있습니다. 만약  localhost:3000/study로 접근하게 된다면 404 오류가 발생하게 됩니다. 방금 있었던 @Get() 데코레이터의 경로를 'Tristy'로 바꾸었기 때문에 기존의 경로가 없어져 버린 것이죠. 그렇기 때문에 해당 엔드 포인트를 찾지 못했고 404 오류가 발생한 것입니다. 만약, localhost:3000/study로 다시 접근할 수 있게 하고 싶다면, @Get() 데코레이터를 하나 더 추가해서 새로운 엔드 포인트를 만들던가, 방금 바꾼 경로를 다시 빈칸으로 만들어 주시면 됩니다.

 

 

Nest의 HTTP 요청 메소드 데코레이터를 이용한 경로 설정이 가능하다.

 

 

기본적으로 내장 메소드를 사용할 때, 자바스크립트 객체 혹은 배열을 반환할 때 자동으로 JSON으로 직렬화됩니다. 그러나 자바스크립트 기본 타입인 string, number, boolean을 반환할 경우에는 Nest가 직렬화를 하지 않고 값만 그대로 내보내게 됩니다.  단순히 문자열만 리턴했을 때는 JSON으로 직렬화를 수행하지 않지만, 아래와 같이 코드를 변경하고 서버를 실행하여 확인해보면 JSON으로 직렬화된 것을 확인할 수 있습니다.

 

참고로 직렬화란, 객체를 연속된 문장이나 연속된 바이트 형태로 바꾸는 행위를 말합니다. 객체는 메모리에 존재하고 추상적이기 때문에 네트워크를 통해 전송하기에는 바람직하지 않거든요. 따라서 객체를 잘 포장해서 전송하기 위해 JSON으로 직렬 화해서 전송을 하고, 브라우저는 역직렬화 과정을 통해 해당 JSON을 다시 객체 타입으로 읽게 됩니다. 

 

JSON으로 잘 직렬화 되었습니다.

 

 

 

📣 Route wildcards

 

Nest js는 패턴 기반 라우트 설정도 지원해줍니다. 와일드카드 패턴을 이용해서 해당 문자 조합과 일치할 경우, 엔드 포인트에 접근하게 할 수 있습니다. *의 경우 길이가 0 이상인 임의의 문자열과 대응하고, ?의 경우에는 임의의 한 문자에 대응합니다. 예제의 경우 ab라는 문자열 뒤에 몇 개의 문자열이 오든 간에 전부 해당 엔드 포인트에 접근하게 됩니다.

 

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Get('ab*')
	WildcradsTest() {
		return 'hello wildcards';
	}
}

 

 

 

 

 

 

 

 

 

 

📣 Status Code

 

 

Nest에서 응답 상태 코드는 201을 사용하는 Post를 제외하고는 항상 기본적으로 200으로 설정됩니다. 방금은 메소드에 응답 코드 변경을 위한 설정을 해주지 않았기 때문에, 디폴트로 200 응답 코드가 반환되었습니다.  만약 바꾸고 싶다면 @HttpCode() 데코레이터를 추가해 상태 코드를 바꿀 수 있습니다. 201로 바꾸고 insomnia를 확인해보면 잘 변경된 것을 확인할 수 있습니다. 다만, HTTP 응답 코드들은 저마다 뜻이 존재하기 때문에 변경할 때는 신중하게 변경하는 것이 좋습니다. 201 응답 코드의 경우에는 요청이 성공적으로 처리되었고, 새로운 자원이 만들어진 경우에 사용하는 응답 코드인데, 바꾼 코드에서는 새로운 자원을 만드는 행위가 없었기 때문에 원래라면 사용하면 안 되는 코드입니다. 예제는 예제일 뿐!

 

응답 코드가 200 → 201로 변경되었습니다.

 

 

📣 Request Object

 

가끔 클라이언트의 요청(Request) 세부 정보에 접근하거나, 해당 데이터를 사용해야 하는 경우가 있습니다. Nest는 기본적으로 이러한 요청 객체에 대한 접근법을 제공해줍니다. 작성한 앤드 포인트에 Nest에서 제공해주는 데코레이터를 추가하고, 주입하도록 지시해서 해당 요청 객체에 접근할 수 있는데요. 이를 위해 Nest에서 제공해주는 데코레이터는 다음과 같습니다.

 

 

@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]

POST 방식으로 요청했을 경우 body 값을 담는 데코레이터 입니다.
@Query(key? : string) req.query / req.query[key]

GET 방식으로 요청했을 경우 쿼리 스트링 값을 담는 데코레이터 입니다.
@Headers(key? : string) req.headers / req.headers[name]

HTTP의 Header 정보를 가져오는 데코레이터 입니다.
@Ip() req.ip

클라이언트의 IP Address를 가져오는 데코레이터 입니다.
@HostParam() req.hosts

요청 호스트의 이름을 반환하는 데코레이터 입니다.

 

이 중에서 간단하게 몇 가지만 살펴보도록 하겠습니다.

 

 

 

 

✍ @Req() & @Res()

@Req() 데코레이터는 요청(request) 객체를 뜻하고, @Res()는 응답(response) 객체를 뜻합니다. express에서 타입을 가져올 수 있는데, Nest js 특성상 express와 fastify 합쳐져 있기 때문에 만약 Nest js의 express에서 fastify로 전환할 경우에는 해당 데코레이터들은 전환되지 않는 문제가 발생합니다. 그래서 잘 사용하지는 않습니다.

 

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Get('test1')
	test1(@Req() request: Request, @Res() response: Response) {
		console.log(request);
		console.log(response);
		return 'test';
	}
}

길어도 너무 긴 request 객체와 response 객체

 

✍ @Param()

@Param() 데코레이터는 라우트 파라미터 값을 나타내는 데코레이터입니다. 아래와 같이 @Get() 데코레이터에 기존의 라우팅 설정과는 달리 쌍점을 사용해서 라우팅을 해주면 @Param 데코레이터에서는 해당 라우트 파라미터를 인식하고 나타내 줍니다. 여기서 주의할 점은 반드시 HTTP 요청 메소드 데코레이터에 설정된 값과 @Param()에 들어가는 값이 똑같아야 합니다. 그렇지 않으면 파라미터 값을 찾을 수 없습니다.

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Get(':id')
	ParamTest(@Param('id') id: number) {
		return id;
	}
}

 

 

 

@Param() 데코레이터를 사용하실 때는 한 가지 더 주의사항이 있습니다. 같은 HTTP 요청 메소드 데코레이터를 사용하고 있고 같은 깊이를 갖는 엔드 포인트가 있을 때, 쌍점(:)을 사용한 라우팅 코드가 먼저 나온다면 원치 않는 API에 접근할 수 있습니다. 만약 study/test 1이라는 경로의 API를 찾으려고 할 때, StudyController의 코드가 이러한 상황이라고 가정해보겠습니다. 이 경우 'test 1'에 진입하고 싶었겠지만, 위에서 ':id'같은 라우팅 경로를 @Param() 데코레이터로 찾는 코드가 있을 경우에는 'test 1'을 라우팅 경로로 인식해서 ParamTest()로 진입하게 됩니다.  그렇기 때문에 되도록이면 쌍점(:)을 사용한 라우팅 코드는 코드의 밑부분에 두시는 편이 좋습니다.

 

@Controller('study')
export class StudyController {
	constructor(private readonly studyService: StudyService){}
    
	@Get(':id')
	ParamTest(@Param('id') id: number) {
		return id;
	}
    
	@Get('test1')
	test1(@Req() request: Request, @Res() response: Response) {
		console.log(request);
		console.log(response);
		return 'test';
	}
}

 

 

 

 

 

✍ @Query()

@Query() 데코레이터는 GET 방식으로 요청했을 경우 쿼리 스트링 값을 담는 데코레이터입니다. 쿼리 스트링은 URL 주소 뒤에 ?와 &값을 사용해 데이터를 전달하는 방식인데요. 밑의 코드를 사용하고 URL에 해당값을 담아서 보내주면 @Query() 데코레이터를 사용하여 값을 가져올 수 있습니다. 다만, 쿼리 스트링 방식의 경우는 URL에 데이터가 담겨서 전송되는 것이기 때문에 비밀번호와 같은 인증 데이터를 다룰 때는 적합하지 않습니다.

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Get()
	QueryTest(@Query('id') id: string, @Query('name') name: string) {
		return id + name;
	}
}

 

✍ @Body()

@Body() 데코레이터는 POST 방식으로 요청했을 경우 body값을 나타내는 데코레이터입니다. 클라이언트가 서버에 POST 방식으로 요청을 할 때 body 값에 key-value 값을 전달해서 보내주면 됩니다. 그러면 @Body() 데코레이터를 통해 해당 값을 사용할 수 있게 됩니다. GET 방식은 body값이 존재하지 않기 때문에 사용할 수 없습니다. 예제처럼 단일 값으로 가져올수도 있고, DTO를 통해 여러 값을 가져올 수도 있습니다. 

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Post()
	BodyTest(@Body() name: string) {
		return name;
	}
}

 

 

 

 

 

📣 Redirection

@Redirect() 데코레이터는 특정 URL로 리디렉션 시켜버리는 데코레이터 입니다. @Redirect() 데코레이터는 두 개의 인자를 받을 수 있습니다. 첫 번째는 리디렉션 할 주소, 두 번째는 상태 코드입니다. 해당 상태 코드는 200, 201, 301, 307 등 여러 가지를 사용할 수 있으나, 리디렉션 응답 코드인 301, 307, 308 등이 아닐 경우에는 브라우저에서 제대로 반응하지 못할 수 있습니다. 밑의 예제에서는 localhost:3000/study/redirect/naver? id=1로 보내면 naver로 리디렉션 되고, 그게 아닐 경우에는 daum으로 리디렉션 됩니다.

 

만약 밑의 코드에서 @Redirect() 데코레이터를 사용하지 않고 서버를 구동하면, 리디렉션 시킬 URL이 JSON 형식으로 출력되니 조심하세요.

 

import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Get('redirect/naver')
	@Redirect('https://www.naver.com/', 302)
	getDocs(@Query('id') id) {
		if (id === '1') {
			return { url: 'https://www.naver.com/' };
		} else {
			return { url: 'https://www.daum.net/' };
		}
	}
}

 

 

 

📣 Sub-Domain Routing

@Controller() 데코레이터는 들어오는 요청의 HTTP 호스트 값이 특정값과 일치하도록 host 옵션을 사용할 수 있습니다. 현재 app.controller.ts에는 디폴트 도메인이 이미 존재하는 상황에서, URL 요청이 다르게 왔을 경우 처리하고 싶을 때는 해당 기능을 유용하게 사용할 수 있습니다. 먼저 sub라는 새로운 컨트롤러를 생성해 줍니다. 

 

$ nest g co sub
import { Controller, Get } from '@nestjs/common';

@Controller({ host: 'api.localhost' })
export class SubController {
	@Get()
	getHello(): string {
		return 'hello Nest!';
	}
}

 

그리고 app.module.ts의 controllers의 순서를 SubController가 먼저 오게 바꿔주세요. 이미 AppController에 같은 @Get() 라우팅 경로가 존재하기 때문에 SubController가 먼저 처리될 수 있게 바꿔주셔야 작동합니다. 이렇게 처리해 주면 같은 엔드포인트여도 하위 도메인을 기술함으로써 다르게 처리할 수 있습니다.

 

SubController의 @Get()
AppController의 @Get()

 

 

 

다만 Fastify에는 중첩 라우터에 대한 지원이 없기 때문에 하위 도메인 라우팅 기능을 사용할 때는, Express 어댑터를 대신 사용해야만 합니다.

 

 

 

📣 Request payloads

 

위에서 @Body() 데코레이터를 설명할 때 DTO 객체를 이용해서도 가져올 수 있다고 말씀드렸었는데요. DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의한 객체입니다. 먼저 파일을 하나 생성해서 DTO를 만들어 줍니다. 간단하게 name과 id만을 받는 DTO를 만들어보았습니다. 클라이언트에서 해당 서버로 데이터를 전송할 때 DTO 규칙을 지키지 않으면 에러가 나기 때문에 반드시 형식을 지켜서 Body에 담아야 합니다.

 

export class TestDto {
	name: string;
	id: string;
}
import { Controller, Get, HttpCode, Param, Req, Res} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('study')
export class StudyController {
	...

	@Post('dtoTest')
	DtoTest(@Body() testDto: TestDto) {
		return testDto;
	}
}