본문 바로가기
BE/Node

Nest.js(2)

by Chars4785 2022. 6. 23.

컨트롤러(Controller)

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

 

컨트롤러는 서버로 들어오는 요청을 처리하고 응답을 가공합니다. 서버에서 제공하는 기능을 어떻게 클라이언트와 주고 받을 지에 대한 인터페이스를 정의하고 데이터의 구조를 기술합니다.

 

컨트롤러는 엔드포인트 라우팅(routing) 메커니즘을 통해 각 컨트롤러가 받을 수 있는 요청을 분류합니다. 컨트롤러를 사용 목적에 따라 구분하면 구조적이고 모듈화된 소프트웨어를 작성할 수 있습니다.

 

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

$ nest g resource [name]

 

만들고자 하는 리소스의 CRUD 보일러 플레이트 코드를 한 번에 생성할 수도 있습니다. 만약 nest g resource Users 명령으로 Users 리소스를 생성하면 다음과 같이 module, controller, service, entity, dto 코드와 테스트 코드를 자동 생성해 줍니다.

컨트롤러 자동 생성

$ nest g controller [name]

💡 CLI로 자동 생성된 update는 PATCH 메서드를 사용하고 있습니다. http 메서드에는 업데이트 동작을 기술하는 메서드가 2가지 있습니다. PUT은 리소스 전체를 교체할 때 쓰고, PATCH는 리소스의 일부를 업데이트 할 때 사용합니다. 하지만 실제 구현시에는 이를 엄격하게 지키지 않고 PUT을 보통 사용하지만, 만약 PATCH가 사용됐다면 이같은 뜻을 가진다고 생각하면 됩니다.

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

 @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

"http://localhost:4000/users" 으로 요청하면 된다.

라우팅(routing)

getHello 함수는 @Get 데코레이터를 가지고 있습니다. 루트 경로('/'가 생략됨)로 들어오는 요청을 처리할 수 있게 되었습니다. 라우팅 경로를 @Get 데코레이터의 인자로 관리할 수 있습니다. 경로를 루트 경로가 아니라 /hello로 변경해 봅시다.

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

- 와일드 카드를 이용해서 여러 경로로 받을수도 있다.

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

위 코드는 helo , hello, he__lo 와 같은 경로로 요청을 받을 수 있습니다. * 외에 ?, +, () 문자 역시 정규 표현식에서의 와일드 카드와 동일하게 동작합니다.

요청 객체

클라이언트에서 보내는 데이터를 핸들러 ( 요청을 처리할 구성요소, 컨트롤러가 이 역할을 한다. ) 가 다룰 수 있는 객체로 변환합니다.

@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();
  }
}

req 는 express 문서 확인  에서 나와있는 내용들을 갖고 있습니다. 

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

응답

객체를 리턴한다면 직렬화를 통해 JSON 으로 자동 변환해 줍니다. 이 방법이 권장하는 방법이긴 하지만 라이브러리별 응답 객체를 직접 다룰 수도 있습니다.

@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);
}

 

{
  "statusCode": 400,
  "message": "id는 0보다 큰 값이어야 합니다.",
  "error": "Bad Request"
}

헤더

Nest는 응답 헤더 역시 자동 구성해 줍니다. 응답에 커스텀 헤더를 추가하고 싶다면 @Header 데코레이터를 사용하면 됩니다. 인자로 헤더 이름과 값을 받습니다. 물론 라이브러리에서 제공하는 응답객체를 사용해서 res.header() 메서드로 직접 설정도 가능합니다.

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

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

리디렉션

종종 서버는 요청을 처리한 후 요청을 보낸 클라이언트를 다른 페이지로 이동시키고 싶은 경우가 있습니다. 응답 본문에 redirectUrl을 포함시켜 클라이언트가 스스로 페이지를 이동해도 되지만, @Redirect 데코레이터를 사용하면 쉽게 구현이 가능합니다.

예를 들어 쿼리 파라미터로 버전 숫자를 전달받아 해당 버전의 페이지로 이동한다고 하면 다음처럼 구현할 수 있습니다.

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

참고로 앞서 설명했듯이 Nest는 자바스크립트 객체를 리턴하면 JSON 스트링으로 직렬화를 해서 보내줍니다.

이제 브라우저에서 http://localhost:3000/redirect/docs?version=5 를 입력하면 https://docs.nestjs.com/v5/ 페이지로 이동됩니다. 302 상태 값도 값도 같이 리턴되고 url, statusCode key 값으로 리턴을 받을수 있습니다.

라우트 파라미터

라우트 파라미터는 패스 파라미터라고도 합니다. 라우트 파라미터를 전달받는 방법은 2가지가 있습니다. 먼저 파라미터가 여러 개 전달될 경우 객체로 한번에 받는 방법입니다. 이 방법은 params의 타입이 any가 되어 권장하지 않습니다. 물론 라우트 파라미터는 타입이 항상 string이기 때문에 명시적으로 { [key: string]: string } 타입을 지정해 주어도 됩니다.

@Delete(':userId/memo/:memoId')
deleteUserMemo(@Param() params: { [key: string]: string }) {
  return `userId: ${params.userId}, memoId: ${params.memoId}`;
}

일반적인 방법은 다음 코드처럼 라우팅 파라미터를 따로 받는 것입니다. REST API를 구성할 때 라우팅 파라미터의 개수가 너무 많아지지 않게 설계하는 것이 좋기 때문에 따로 받아도 코드가 많이 길어지지는 않습니다.

@Delete(':userId/memo/:memoId')
deleteUserMemo(
  @Param('userId') userId: string,
  @Param('memoId') memoId: string,
) {
  return `userId: ${userId}, memoId: ${memoId}`;
}

하위 도메인

 

app.controller.ts에 이미 루트 라우팅 경로를 가진 엔드포인트가 존재합니다. ApiController에도 같은 엔드포인트를 받을 수 있도록 할 것인데 이를 위해 ApiController가 먼저 처리될 수 있도록 순서를 수정합니다.

@Module({
  controllers: [ApiController, AppController],
    ...
})
export class AppModule { }

@Controller 데코레이터는 ControllerOptions 객체를 인자로 받는데 host 속성에 하위 도메인을 기술하면 됩니다.

@Controller({ host: 'api.example.com' }) // 하위 도메인 요청 처리 설정
export class ApiController {
  @Get() // 같은 루트 경로
  index(): string {
    return 'Hello, API'; // 다른 응답
  }
}

이제 각각 GET 요청을 보내면 응답을 다르게 주는 것을 볼수 있습니다.

❗ 로컬에서 테스트를 하기 위해 하위 도메인을 api.localhost 로 지정하면 curl 명령어가 제대로 동작하지 않습니다. 이는 api.localhost가 로컬 요청을 받을 수 있도록 설정되어 있지 않기 때문입니다. 이를 해결하기 위해 /etc/hosts 파일의 마지막에 127.0.0.1 api.localhost 을 추가하고 서버를 다시 구동하면 됩니다. 윈도우의 경우도 해결방법이 있으니 다른 자료를 참고바랍니다

페이로드 다루기

POST, PUT, PATCH 요청은 보통 처리에 필요한 데이터를 함께 실어 보냅니다. 이 데이터 덩어리(페이로드)를 본문(body)이라고 합니다. NestJS는 본문을 DTO(Data Transfer Object)를 정의하여 쉽게 다룰 수 있습니다.

DAO (Data Access Object)

DB의 data에 접근하기 위한 객체이다. DB에 접근하기 위한 로직을 분리하기 위해 사용한다.

직접 DB에 접근하여 data를 삽입, 삭제, 조회 등 조작할 수 있는 기능을 수행한다.

MVC 패턴의 Model에서 이와 같은 일을 수행한다.

DTO(Data Transfer Object)

DTO는 계층 간(Controller, View, Business Layer) 데이터 교환을 위한 자바 빈즈(Java Beans)를 의미한다.
DTO는 로직을 가지지 않는 데이터 객체이고 getter/setter 메소드만 가진 클래스를 의미한다.

export class CreateUserDto {
  name: string;
  email: string;
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
  const { name, email } = createUserDto;

  return `유저를 생성했습니다. 이름: ${name}, 이메일: ${email}`;
}

 

GET 요청에서 서버에게 전달할 데이터를 포함할 때는 일반적으로 요청 주소에 포함시킵니다. 예를 들어 유저 목록을 가져오는 요청은 GET /users?offset=0&limit=10 과 같이 페이징 옵션이 포함되도록 구성할 수 있습니다. offset은 데이터 목록 중 건너뛸 개수를 의미하고 limit은 offset 이후 몇 개의 데이터를 가져올 지 결정합니다. 이 두개의 쿼리 파라미터를 @Query DTO로 묶어 처리할 수 있습니다.

export class GetUsersDto {
  offset: number;
  limit: number;
}

 


참고

https://wikidocs.net/148197

'BE > Node' 카테고리의 다른 글

Nest.js(4)  (0) 2022.06.29
관점 지향 프로그래밍(AOP)  (0) 2022.06.24
Nest.js(1)  (0) 2021.11.12
[Mongoose] 스키마  (0) 2021.09.24
[Node] Fastify  (0) 2020.03.13

댓글