[NestJS] Controller

Devhslee·2023년 10월 14일

NestJS 기초

목록 보기
4/6

Controller는 NestJS에서 client의 요청을 받아서 응답을 되돌려주는 역할을 하는 객체이다.

NestJS 내부적으로 라우팅 매커니즘을 사용하여 어떤 controller가 어떤 요청을 처리할 지를 결정한다.

@Controller

Controller는 @Controller 데코레이터로 지정된 클래스이다.

NestJS 프로젝트를 생성할 때 기본적으로 만들어지는 AppController가 다음과 같다.

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데코레이터의 파라미터로는 경로명을 뜻하는 prefixControllerOptions가 전달될 수 있다. ControllerOptions를 자세히 살펴보면 다음과 같다.

export interface ControllerOptions extends ScopeOptions, VersionOptions {
    path?: string | string[];
    host?: string | RegExp | Array<string | RegExp>;
}

pathprefix랑 같은 역할인데,

요컨데

@Controller('user')
export class UserController {}

이렇게 적으나

@Controller({ path: 'user' })
export class UserController {}

저렇게 적으나 차이는 없다.

host는 요청을 보내는 client의 host를 제한하고 싶을 때 사용한다.

예를 들어

@Controller({ path: 'user', host: 'client.example.com' })
export class UserController {}

위와 같이 지정할 경우, UserController는 'client.example.com'이라는 호스트 클라이언트에서만 '/user'로 들어오는 요청을 받게 된다.


Controller 생성하기

nest cli로 모듈을 생성했듯 controller 역시 생성할 수 있다.

$ nest g controller User --no-spec

controller 말고 co 로 줄여써도 결과는 똑같다.

$ nest g co User --no-spec

--no-spec옵션은 테스트 파일을 같이 만들지 않기 위해서이다. 저 옵션을 주지 않으면 controller에 대한 e2e 테스트 파일(~~.controller.spec.ts)이 같이 만들어진다.


(기존에 UserModule을 이미 만들었다고 가정)
어쨌거나 위의 명령어로 UserController를 생성하면 다음과 같은 두 개의 변화가 일어난 걸 볼 수 있다.

1) /user 내에 user.controller.ts 파일 생성

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

@Controller('user')
export class UserController {}

2) UserModule의 controllers 부분에 UserController 자동 추가

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  imports: [],
  providers: [],
  controllers: [UserController],
})
export class UserModule {}

nest cli로 특정 이름의 controller를 생성하게 되면 @Controller의 path에 해당 이름으로 경로가 설정된다.

따라서 UserController는 '/user/'로 시작되는 경로로 들어온 요청에 매핑이 되는 것이다.


HTTP 메서드

특정 메서드를 Request handler로 지정하고 싶다면 해당 핸들러가 처리하려는 HTTP 메서드에 걸맞는 decorator로 지정해주면 된다.

@Get → GET

import { Controller, Get } from '@nestjs/common';

@Controller('user')
export class UserController {
  
  @Get()
  async getHello() {
    return 'GET method';
  }
}

@Post → POST

import { Controller, Post } from '@nestjs/common';

@Controller('user')
export class UserController {
  
  @Post()
  async postHello() {
    return 'POST method';
  }
}

@Put → PUT

import { Controller, Put } from '@nestjs/common';

@Controller('user')
export class UserController {
  
  @Put()
  async putHello() {
    return 'PUT method';
  }
}

@Delete → DELETE

import { Controller, Delete } from '@nestjs/common';

@Controller('user')
export class UserController {
  
  @Delete()
  async deleteHello() {
    return 'DELETE method';
  }
}

@Patch → PATCH

import { Controller, Patch } from '@nestjs/common';

@Controller('user')
export class UserController {
  
  @Patch()
  async patchHello() {
    return 'PATCH method';
  }
}

그 외 @Options, @Head도 사용할 수 있다.


요청 처리하기 (라우팅 파라미터)

client가 요청을 보낼 때 부가적인 데이터를 query string에 포함시켜 보낼 수도 있고, params로 보낼 수도 있고, body 안에 담아서 보낼 수도 있다.

1) @Query

예를 들어 사용자가 '/user?id=24' 로 GET 요청을 보낼 경우, 경로 뒤에 붙는 id 값을 받기 위해 @Query 데코레이터를 사용할 수 있다.

import { Controller, Get, Query } from '@nestjs/common';

@Controller('user')
export class UserController {
  @Get()
  async getUser(@Query('id') id: number) {
    return `User Id is ${id}`;
  }
}

코드를 로컬로 실행해서 주소창에 'localhost:3000/user?id=24'를 치면 다음과 같은 결과를 얻을 수 있다.

2) @Param
위의 경우는 id를 쿼리스트링으로 넘긴 경우이고, param으로도 받을 수 있다.
사용하기 위해서는 HTTP 메서드 데코레이터 안에 ':{파라미터명}'을 써 주고 해당 파라미터를 받을 것임을 @Param 안에 명시해주어야 한다.

import { Controller, Get, Param } from '@nestjs/common';

@Controller('user')
export class UserController {
  @Get(':id')
  async getUser(@Param('id') id: number) {
    return `User Id is ${id}`;
  }
}

그리고 주소창에 'localhost:3000/user/24'를 치면 위와 똑같은 결과를 얻을 수 있다.

3) @Body
위의 두 방식은 경로에 어떤 파라미터를 넣는지 url 상에서 고스란히 노출되는 데다가,
서버에 넘길 정보가 많아지면 url도 마찬가지로 길어지게 된다.

가령 게시판의 게시글을 저장해야 하는 경우,
제목, 내용, 첨부파일, 예약시간, 작성자 등의 많은 정보를 넘겨받아야 하고
넘겨받은 정보에 대한 validation도 진행해야 한다.

이럴 경우 body 안에 정보를 담아 서버로 보내야 한다. 받고자 하는 body를 통채로 받기 위해 @Body 데코레이터를 사용한다.

import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('user')
export class UserController {
  @Post('/create')
  async createUser(@Body() body: CreateUserDto) {
    return body;
  }
}

응답 반환하기

위에서 작성한 예시 코드들은 모두 명시하지 않아도 디폴트로 200 응답을 주고 있다.
(단, POST 요청의 성공 응답코드는 201이다)

또한 NestJS는 request handler가 반환하는 타입에 따라 JSON 직렬화 유무를 결정한다.
1) Javascript 원시 타입 (e.g. string, number, boolean) -> X
2) Javascript 객체나 배열 -> O

가령 다음과 같이 코드를 작성하고

import { Controller, Get, Param } from '@nestjs/common';

@Controller('user')
export class UserController {
  @Get(':id')
  async getUser(@Param('id') id: number) {
    return { id, name: 'sample user' };
  }
}

결과를 받아오면

위와 같이 자동으로 JSON으로 변환하여 리턴해주고 있는 것을 볼 수 있다.
(나는 Chrome에 JSON Formatter를 추가해서 위와 같이 보이는 것이다)


만약에 응답 코드를 다르게 주고 싶다면 어떻게 해야 할까?
그럴 땐 @HttpCode 데코레이터를 사용해주면 된다.

import { Controller, Get, Param, HttpCode } from '@nestjs/common';

@Controller('user')
export class UserController {
  @Get(':id')
  @HttpCode(204)
  async getUser(@Param('id') id: number) {
    return { id, name: 'sample user' };
  }
}

위의 코드는 성공응답 코드를 204로 바꾼 것이다. postman에서 이 요청을 다시 테스트 해보면

성공적으로 응답 코드가 바뀌어 온 것을 볼 수 있다.


요청/응답 객체를 Express에서 쓰이는 걸로 할 경우

위에서 요청/응답을 구현하기 위해 쓴 데코레이터들은 모두 NestJS에서 제공하는 것들이다.

만약 이들을 쓰지 않고 기존의 Express나 Fastify에서 사용했던 객체를 쓰면 어떨까?

위에서 query string으로 id를 받아서 출력했던 예를 @Req, @Res 데코레이터를 사용하여 다시 작성해보겠다.

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

@Controller('user')
export class UserController {
  @Get()
  async getUser(@Req() req: Request, @Res() res: Response) {
    const query = req.query;
    const id = query.id;
    res.send(`User Id is ${id}`);
  }
}

위에서 들었던 예시와 결과는 똑같이 나온다. 다른 점은 쿼리스트링을 @Query로 받아온 게 아니라 Express에서 사용하는 Request 타입으로 받아 거기서 query를 추출해내서 사용했고, 결과를 그대로 리턴하는 것이 아니라 Response객체의 send() 메서드로 응답값을 넣어주었다는 것이다.

이게 가능한 이유는 NestJS가 Express를 기반으로 하여 만들어진 프레임워크이기 때문이다.

단, @Res @Next 와 같이 Express나 fastify에 맞춰진, Library-specific한 응답 객체를 사용하게 되면, 그 즉시 해당 데코레이터가 주입된 라우트 핸들러는 Library-specific mode가 되어버리기 때문에 기존의 NestJS 핸들러가 return 하는 방식을 사용하면 에러가 난다.

가령,

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

@Controller('user')
export class UserController {
  @Get()
  async getUser(@Req() req: Request, @Res() res: Response) {
    const query = req.query;
    const id = query.id;
    //res.send(`User Id is ${id}`);
    return `User Id is ${id}`;
  }
}

getUser()에 @Res를 주입하고 정작 NestJS에서 일반적으로 응답을 리턴하는 방식을 써버리면 정상적인 응답이 오지 않는다(계속 펜딩되다가 결국 오류).

따라서 @Res를 사용하게 되면 응답을 어떻게 구성해서 줄지는 온전히 개발자의 책임이 되므로 주의해서 사용하는 것이 좋다.


profile
코딩-버그-좌절-해결-희열

0개의 댓글