본문으로 바로가기

NestJS 컨트롤러(Controller) [1]

category JavaScript/Nest 2022. 8. 17. 15:38

컨트롤러(Controller)

Nest의 컨트롤러는 MVC패턴에서 말하는 그 컨트롤러를 말합니다.

컨트롤러는 들어오는 요청(request)를 받고 처리된 결과를 응답(response)으로 돌려주는 인터페이스 역할을 합니다.

https://wikidocs.net/148192

컨트롤러는 엔드포인트 라우팅(routing) 메커니즘을 통해 각 컨트롤러가 받을 수 있는 요청을 분류합니다.

컨트롤러를 사용 목적에 따라 구분하면 구조적이고 모듈화된 소프트웨어를 작성할 수 있습니다.

 

소스코드를 직접 작성해도 되지만, 설치한 nest CLI를 이용하여 쉽게 생성할 수 있습니다.
$ nest g controller [이름]

자동 생성된 코드를 보면 AppController는 AppModule에 선언되어 있고, AppService를 import해서 사용하고 있습니다.

 

다른 Nest 구성요소에 대한 약어는 nest -h 명령어로 확인할 수 있습니다.

 

만들고자 하는 리소스의 CRUD 보일러 플레이트 코드를 한 번에 생성할 수도 있습니다.

$ nest g resource [이름]

만약 nest g resource Users 명령으로 Users 리소스를 생성하면 다음과 같이 module, controller, service, entity, dto 코드와 테스트 코드를 자동 생성해줍니다.

 

 

라우팅(routing)

이미 우리는 서버를 구동시키고 동작하는 것을 확인해 보았습니다.

localhost의 루트경로로 요청 처리가 되고 있다는 뜻입니다.

소스코드를 살펴보겠습니다.

 

  • app.controller.ts
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 데코레이터를 클래스에 선언하는 것으로 해당 클래스는 컨트롤러 역할을 하게 됩니다.

 

getHello 함수는 @Get 데코레이터를 가지고 있습니다. 루트 경로(' / '가 생략됨)로 들어오는 요청을 처리할 수 있게 되었습니다.

라이퉁 경로를 @Get 데코레이터의 인자로 관리할 수 있습니다.

경로를 루트 경로가 아니라 /hello로 변경해 봅시다.

@Get('/hello')
getHello2(): string {
  return this.appService.getHello();
}

그리고 다시 루트 경로로 요청을 보내면 404 Not found 에러를 만나게 됩니다.

{
    "statusCode": 404,
    "message": "Cannot GET /",
    "error": "Not Found"
}

http://localhost:3000/hello 로 주소를 변경하면 정상 동작하는 것을 확인할 수 있습니다.

 

@Controller 데코레이터에도 인자를 전달할 수 있습니다.

이를 통해 라우팅 경로의 prefix를 지정합니다.

예를 들어 @Controller('app')이라고 했다면, 이제 http://localhost:3000/app/hello 라는 경로로 접근해야 합니다.

prefix는 보통 컨트롤러가 맡은 리소스의 이름을 지정하는 경우가 많습니다.

 

 

와일드카드 사용

라우팅 패스는 와일드카드를 이용하여 작성할 수 있습니다.

예를 들어 별표(*) 문자를 사용하면 문자열 가운데 어떤 문자가 와도 상관없이 라우팅 패스를 구성하겠다는 뜻입니다.

@Get('he*lo')
getHello(): string {
  return this.appService.getHello();
}

위 코드는 helo, hello, he__lo와 같은 경로로 요청을 받을 수 있습니다.

* 외에 ?, +, ( ) 문자 역시 정규 표현식에서의 와일드 카드와 동일하게 동작합니다.

단, 하이픈(-)과 점(.)은 문자열로 취급합니다. 즉, @Get('he.lo')는 hello로 요청할 수 없습니다.

 

와일드 카드는 컨트롤러의 패스를 정할 때만 사용하는 것이 아닙니다.
컴포넌트에서 이름을 정할 때 사용할 수 있습니다.

 

 

요청 객체(Request Object)

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

Nest는 요청과 함께 전달되는 데이터를 핸들러(요청을 처리할 구성요소, 컨트롤러가 이 역할을 합니다)가 다룰 수 있는 객체로 변환합니다.

이렇게 변환된 객체는 @Req() 데코레이터를 이용하여 다룰 수 있습니다.

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

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

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

요청 객체는 HTTP 요청을 나타냅니다.

요청 객체(req)가 어떻게 구성되어 있는지 console에 출력해보세요.

쿼리 스트링, 파라미터, 헤더와 본문 외 많은 정보를 가지고 있습니다.

더 자세한 내용은 Express 문서를 참고하세요.

 

API를 작성할 때 요청 객체를 직접 다루는 경우는 드뭅니다.

Nest는 @Query(), @Param(key?: string), @Body 데코레이터를 이용해서 요청에 포함된 쿼리 파라미터, 패스 파라미터, 본문을 쉽게 받을 수 있도록 해줍니다.

 

 

응답

앞에서 nest g resource Users 명령어로 Users 리소스에 대한 CRUD API를 만들어 보았습니다.

서버를 실행하면 어떤 라우팅 패스를 통해 요청을 받을 수 있는지 콘솔 로그를 통해 확인할 수 있습니다.

...
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RoutesResolver] UsersController {/users}: +0ms
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RouterExplorer] Mapped {/users, POST} route +1ms
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RouterExplorer] Mapped {/users, GET} route +0ms
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RouterExplorer] Mapped {/users/:id, GET} route +1ms
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RouterExplorer] Mapped {/users/:id, PATCH} route +0ms
[Nest] 11780 - 2021-09-12 15:01:08 LOG [RouterExplorer] Mapped {/users/:id, DELETE} route +0ms
...

이를 각각 요청하면 다음과 같은 결과가 나옵니다.

  • Users 리소스에 대한 CRUD 요청 결과
경로 http method 응답 상태 코드 body
/users POST 201 This action adds a new user
/users GET 200 This action returns all users
/users/1 GET 200 This action returns a #1 user
/users/1 PATCH 200 This action updates a #1 user
/users/1 DELETE 200 This action removes a #1 user
CLI로 자동 생성된 update는 PATCH 메서드를 사용하고 있습니다.
http 메서드에는 업데이트 동작을 기술하는 메서드가 2가지 있습니다.
PUT은 리소스 전체를 교체할 때 쓰고, PATCH는 리소스의 일부를 업데이트 할 때 사용합니다.
하지만 실제 구현시에는 이를 엄격하게 지키지 않고 PUT을 보통 사용하지만,
만약 PATCH가 사용됐다면 이같은 뜻을 가진다고 생각하면 됩니다.

각  요청의 성공 응답 코드는 POST일 경우에만 201이고, 나머지는 200인 것을 볼 수 있습니다.

또한 응답 본문은 스트링 값을 가지고 있는데 이는 UsersController의 각 메서드가 리턴하는 값입니다.

Nest는 이렇게 응답을 어떤 방식으로 처리할 지 미리 정의해두었습니다.

string, number, boolean과 같이 자바스크립트 원시 타입을 리턴할 경우 직렬화 없이 바로 보내지만,

객체를 리턴한다면 직렬화를 통해 JSON으로 자동 변환해줍니다.

이 방법이 권장하는 방법이긴 하지만 라이브러리별 응답 객체를 직접 다룰 수도 있습니다.

예를 들어 Express를 사용한다면 Express response object @Res 데코레이터를 이용해서 다룰 수 있습니다.

@Get()
findAll(@Res() res) {
  const users = this.usersService.findAll()

  return res.status(200).send(users);
}

앞서 Nest는 CRUD에 대해 성공 응답으로 POST는 201, 그 외에는 200을 가진다고 했습니다.

만약 이 상태코드를 다른 값으로 바꾸길 원한다면 어떻게 해야 할까요?

Nest는 이를 손쉽게 적용할 수 있는 또 다른 데코레이터 @HttpCode를 마련해두었습니다.

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

@HttpCode(202)
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
  return this.usersService.update(+id, updateUserDto);
}
HTTP 202 Accepted는 요청이 성공적으로 접수되었으나, 아직 해당 요청에 대해 처리 중이거나 처리 시작 전임을 의미합니다.
요청이 처리 중 실패할 수도 있기 때문에 요청은 실행될 수도 실행되지 않을수도 있습니다.
이 상태 코드는 비확약적, 즉 HTTP가 나중에 요청 처리 결과를 나타내는 비동기 응답을 보낼 방법이 없다는 것을 의미합니다.
이는 다른 프로세스나 서버가 요청을 처리하는 경우 또는 일괄 처리를 위한 것입니다. - MDN Web Docs

만약 요청을 처리하는 도중 에러가 발생하거나 예외를 던져야 한다면 어떻게 할까요?

예를 들어 유저 정보 조회(GET /users/:id) 요청 했는데 id는 1부터 시작하는 규칙을 가지고 있다고 하겠습니다.

만약 id가 1보다 작은 값이 전달될 경우 400 Bad Request 예외를 던져야 합니다.

@Get(':id')
findOne(@Param('id') id: string) {
  if (+id < 1) {
    throw new BadRequestException('id는 0보다 큰 값이어야 합니다.');
  }

  return this.usersService.findOne(+id);
}

id를 0으로 요청한 결과입니다.

$ curl -X GET http://localhost:3000/users/0
{
  "statusCode": 400,
  "message": "id는 0보다 큰 값이어야 합니다.",
  "error": "Bad Request"
}

NotFoundException 객체의 생성자로 전달한 메세지와 함께 상태코드가 400인 에러가 발생합니다.

터미널에서 curl 명령어로 실행하면 위와 같이 JSON 결과가 읽기 편하게 출력되지 않습니다.
출력을 jq에 파이프로 전달하면 되는데, 이는 설명의 편의상 생략합니다.
ex) $ curl -X GET http://localhost:3000/users/0 | jq

 

 

헤더

Nest는 응답 헤더 역시 자동 구성해줍니다.

https://wikidocs.net/148201

만약 응답에 커스텀 헤더를 추가하고 싶다면 @Header 데코레이터를 사용하면 됩니다.

인자로 헤더 이름과 값을 받습니다.

물론 라이브러리에서 제공하는 응답객체를 사용해서 res.header() 메서드로 직접 설정도 가능합니다.

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

@Header('Custom', 'Test Header')
@Get(':id')
findOneWithHeader(@Param('id') id: string) {
  return this.usersService.findOne(+id);
}

https://wikidocs.net/148201

curl 명령어를 사용할 때 자세한 정보를 얻으려면 -v (verbose) 옵션을 주면 됩니다.
또한 -X 옵션을 생략하면 GET으로 동작합니다.
-v 옵션을 이용하면 헤더를 확인할 수 있습니다.
$ curl http://localhost:3000/users/1 -v
Note: Unnecessary use of -X or --request, GET is already inferred.
*      Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /users/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Custom: Test Header
< Content-Type: text/html; charset=utf-8
< Content-Length: 29
< ETag: W/"1d-MU9PTdoaF+1jeHzvs+kaeFq7QDs"
< Date: Mon, 27 Sep 2021 01:23:29 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
This action returns a #1 user* Closing connection 0

 

 

 

참고: NestJS로 배우는 백엔드 프로그래밍

반응형