NestJS 빠르게 배우기 01

NestJS 빠르게 배우기 01

사이드 프로젝트를 하기 위해서 백앤드를 여러가지로 고민하고 있었는데, 선택한 바로는 NestJS를 사용하기로 했다. 7.0.0 버전이 릴리즈 됐다고도 하고(현재는 7.0.3이다), star도 많이 받고 있어서 확인해봤다. 그리고 우연하게 직방에서 어떤 스택을 사용하고 있는지 확인할 일이 있었는데, 직방에서도 NestJS를 일부 도입하고 있는 것 같았다. 사이드 프로젝트답게, 개인적으로 도전적인 스택을 고민했는데, 백앤드를 Golang으로 구성해보고 싶었지만, Golang은 조금 더 사용해보면서 코드 스타일에 대해서 연습해보고 싶었다. (솔직히 Golang 코드로 작성되는 모양새가 마음에 쏙 들지는 않음) 지금 마음 속으로는 NestJS, GraphQL, Electron 이렇게 생각하고 있다. 우선 이 글은 NestJS를 도입하기 위한 간단한 NestJS 개념을 정리해보는 글이다.

시작하기

이 글은 NestJS의 Overview를 확인하고 작성하고 있다. 우선 NestJS가 제공해주는 보일러플레이트로 프로젝트를 시작하는 방법은 이 링크에서 간단하게 확인할 수 있다. 시작해보면 간단히 아래와 같은 모습을 프로젝트에서 취하고 있다.

1
2
3
4
src
| main.ts # 앱의 시작점
| app.module.ts # 앱의 루트 모듈
| app.controller.ts # 앱의 컨트롤러

이번 글은 Controller에 대해서 확인해보자.

Controller

controller는 흔히 알려져 있듯, Request, Response를 처리하는 로직이다. 특정 라우터에 붙여서 구체적인 요청을 받아서 처리한다.
Nest에서는 기본 컨트롤러를 만들려면 classdecorators를 사용한다.


공식 문서에서 제공하는 이미지

라우팅

@Controller()를 사용해서 컨트롤러를 지정할 수 있는데, 데코레이터를 통해서 연관된 라우터를 그룹 지을 수 있다. Nest 문서에서 나타난 코드는 아래와 같은데 라우트 패스 접두사를 cats로 정의한 것이다. 이러한 prefix를 지정해서 앞서 말한 관련된 라우터로 그룹 지을 수 있는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
cats.controller.tsJS;

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

@Controller("cats")
export class CatsController {
@Get()
findAll(): string {
// 여기는 유저가 정의하는 부분이다.
return "This action returns all cats";
}
}

@Get()은 Nest에게 HTTP request들의 앤드포인트를 구체적으로 지정하는 역할을 한다. @Get()을 사용해서 위에 컨트롤러에 정해진 prefix와 합쳐서 엔드 포인트를 정할 수 있다. 지금 예시는 GET /cats에서 동작하는 컨트롤러라고 볼 수 있다. request Method 데코레이터에서도 구체적인 엔드 포인트를 지정할 수 있는데, 예를 들어서 GET /cats/spinks 엔드 포인트를 원한다면 해당 메서드에 @Get('spinks')를 사용하면 된다.

위 예시는 200 코드와 응답을 줄 것이다. 일반적인 Express app처럼 res.json을 사용하지 않는데 이건 Nest가 사용하는 두 가지의 Response를 다루는 옵션과 관련 되어 있다.

  • Standard: Nest 문서에서 추천하는 방식이고, 내장된 매서드를 사용하는 것이다. JS Object나, Array로 값을 리턴하면 자동적으로 JSON으로 시리얼라이징 해준다. 하지만 나머지 JS 리터럴 값들은 시리얼라이징 하지 않고 보낸다. 그리고 status code는 항상 200이다 (단, POST 매서드 같은 경우는 201). 당연히 Default 값이라는 뜻이고 이를 변경하려면 @HttpCode(...)를 붙여줘야 한다. 이와 관련해서는 나중에 천천히 알아보자
  • Library-specific: 쉽게 말해서 Express같은 라이브러리의 Response 스타일을 사용하도록 만드는 것이다. 이건 사용하고 싶지 않기 때문에 구체적인 얘기를 건너 뛰도록 하겠다.
  • 주의: 두 가지를 동시에 사용할 수는 없다고 한다.

스탠다드를 추천하고 있으니, 프로젝트나 글에서도 스탠다드 형태로 많이 정리해볼 예정이다.

Request Object

Nest는 Request 객체에 접근하는 방식을 마찬가지로 데코레이터로 제공한다. Default인 Request 객체는 Express의 객체를 따른다.

1
2
3
4
5
6
7
8
9
10
import { Controller, Get, Req } from "@nestjs/common";
import { Request } from "express";

@Controller("cats")
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return "This action returns all cats";
}
}

Express의 request 객체를 사용하지만, Express처럼 직접 querybody를 꺼내오지 않아도 된다. (물론 할 수는 있는 것 같다.) @Body(), @Query()를 사용하면 request 객체에서 가져올 수 있다. 그 밖에 아래와 같은 데코레이터를 지원한다.

1
2
3
4
5
6
7
8
@Request()	-> req
@Response(), @Res() -> res // 위에서 언급한 것처럼, Library-specific한 응답을 보낼 때 사용한다.
@Next() -> next
@Session() -> req.session
@Param(key?: string) -> req.params / req.params[key]
@Body(key?: string) -> req.body / req.body[key]
@Query(key?: string) -> req.query / req.query[key]
@Headers(name?: string) -> req.headers / req.headers[name]

리소스

Nest는 일반적인 HTTP request 엔드포인트 데코레이터를 @Get()과 동일하게 제공하고 있다. - @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(), @All()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// cats.controller.ts
import { Controller, Get, Post } from "@nestjs/common";

@Controller("cats")
export class CatsController {
@Post()
create(): string {
return "This action adds a new cat";
}

@Get()
findAll(): string {
return "This action returns all cats";
}
}

한 가지 개인적으로 궁금한 게 있는데, @Get()@Post() 처럼 여러 메서드를 데코레이터로 설정해둘 수 있을까? 또는 여러 엔드포인트에서 동작하도록 @Get("something"), @Get("another")을 붙여도 동작할까? 프로젝트 하면서 시도해봐야겠다.

와일드카드 라우팅

패턴 기반 라우터도 제공하고 있다. 예를 들어서 아래 애스터리스크 *는 패턴으로 사용된다. abcd, ab_cd, abecd 모두 매칭된다.

1
2
3
4
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}

?, +, *, ()가 라우터 패스에 사용될 수 있다.

Status code

위에서 말한 대로, 기본은 200과 201(201은 POST 요청일 때)이다. 기본을 바꾸기 위해서 @HtppCode(...)를 사용하면 된다.

1
2
3
4
5
6
7
import {HttpCode} from "@nestjs/common"

@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}

다만, 상황 별로 다르게 status code를 보내줘야 하는 경우에는 @Res를 쓰거나, 에러를 throw 해야 한다.

커스텀 헤더

커스텀한 Response Header를 만들기 위해서 @Header()를 사용할 수 있다.

1
2
3
4
5
6
7
import {Header} from "@nest/common"

@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}

리다이렉션

리다이렉트 리스폰스를 주기 위해, @Redirect() 데코레이터를 사용하거나, library-specific 리스폰스 오브젝트를 사용할 수 있다. @Redirect()url를 인자값으로 넘겨줘야 한다. 그리고 선택적으로 statusCode를 넘겨줘야 하는데 기본 값은 302이다.

1
2
@Get()
@Redirect("https://changhoi.github.io", 301)

만약 statusCode와 리다이렉트 할 URL을 동적으로 설정해주고 싶다면, 메서드 안에서 아래와 같은 형태로 리턴을 해주면 된다.

1
2
3
4
{
"url": string,
"statusCode": number
}

이 값이 @Redirect() 데코레이터 인자값으로 넘어간 값을 덮어 쓰게 된다.

1
2
3
4
5
6
7
@Get("docs")
@Redirect("https://docs.nestjs.com", 302)
getDocs(@Query("version") version) {
if (version && version === "7") {
return {url: "https://docs.nestjs.com/v5/"};
}
}

파라메타 라우팅

라우터에 파라메타를 넣는 방법은 Express와 동일하다. 그리고 파라메타를 얻는 방법도 마찬가지로 데코레이터를 사용해서 받아 올 수 있다. 메소드의 파라메타 뿐 아니라 라우터에서 지정된 파라메터도 받아 올 수 있다.

1
2
3
4
5
6
7
8
9
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}

/**
findOne(@Params("id") id): string {} 과 같이 id를 구체적으로 가져올 수도 있다.
*/

서브도메인 라우팅

@Controller 데코레이터는 host 옵션을 가져올 수 있는데, 이 옵션으로 들어오는 요청의 HTTP 호스트를 구체적으로 맞춰 줄 수 있다.

1
2
3
4
5
6
7
@Controller({ host: "admin.example.com" })
export class AdminController {
@Get()
index(): string {
return "admin page";
}
}

route와 마찬가지로, host 옵션은 동적인 값을 호스트 이름에 위치시키기 위해서 토큰처럼 사용할 수도 있다. host 파라미터 토큰은 @Controller 에서 @HostParam() 데코레이터를 통해서 접근할 수 있다.

1
2
3
4
5
6
7
@Controller({ host: ":account.example.com" })
export class AccountController {
@Get()
getInfo(@HostParam("account") account: string) {
return account;
}
}

비동기성

데이터 추출은 JS에서 대부분 비동기로 작동한다 (I/O, Network). Nest에서는 async 함수를 잘 지원한다. 모든 비동기 함수는 Promise를 리턴한다. 즉, Nest가 스스로 resolve 할 수 있는 연기된 값 (미뤄진, Promised된 값)을 전달할 수 있다는 뜻이다. 아래와 같이 작성할 수 있다.

1
2
3
4
@Get()
async findAll(): Promise<any[]> {
return []
}

위의 코드는 완전 유효하지만, Nest route handler에서는 RxJS의 Observable streams를 리턴함으로서 더 강력하게 위 동작을 해낼 수 있다. Nest는 자동적으로 해당 소스를 subscribe하고, 값이 나오면 가져간다.

1
2
3
4
@Get()
findAll(): Observable<any[]> {
return of([])
}

개인적으로 RxJS를 모르기 때문에 비동기를 기쁘게 활용하도록 하려고 한다.

Request payloads

POST 라우트 핸들러가 클라이언트의 payload를 사용하려면 먼저 DTO(Data Transfer Object)를 정의해야 한다. DTO는 어떻게 데이터가 보내지게 될 지 정의하는 객체이다. DTO 스키마를 Typescript의 interfaceclass를 사용해서 정의할 수 있다. Nest에서는 이 객체를 클래스로 정의할 것을 추천한다. 이유는 클래스는 ECMA의 표준에 속하고, 컴파일된 JS에서도 실제 entities로서 다룰 수 있게 된다. 반면 interface는 컴파일 과정에서 삭제되기 때문에 런타임에서 Nest가 참조할 수가 없게 된다. 이러한 차이는 런타임에 이러한 메타데이터(class또는 interface로 정의된 DTO와 같은)에 접근해야 하는 Pipe와 같이 추가적인 가능성들을 더해주는 기능들이 있기 때문에 중요하다. 아래와 같이 DTO를 정의한 다음에 컨트롤러 안에서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}

@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}

abstract class는 어떨까? abstract class는 컴파일 할 때 삭제되는 것이 아니라 abstract가 지워지는 것으로 알고 있는데, 프로퍼티를 initializing 하지 않아도 되니 타입스크립트의 옵션을 끄지 않아도 될 것 같은데.

에러 핸들링

별도의 파트가 있고, 관련된 내용은 이 글에서 확인할 수 있다.

실행해보기

아래는 완전한 예시 샘플이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import {
Controller,
Get,
Query,
Post,
Body,
Put,
Param,
Delete
} from "@nestjs/common";
import { CreateCatDto, UpdateCatDto, ListAllEntities } from "./dto";

@Controller("cats")
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return "This action adds a new cat";
}

@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}

@Get(":id")
findOne(@Param("id") id: string) {
return `This action returns a #${id} cat`;
}

@Put(":id")
update(@Param("id") id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}

@Delete(":id")
remove(@Param("id") id: string) {
return `This action removes a #${id} cat`;
}
}

위와 같이 컨트롤러를 설정한 다음엔, Nest에게 컨트롤러가 있다는 걸 알려줘야 한다. 컨트롤러는 항상 module에 속한다. 그래서 @Module() 데코레이터 안에 controllers Array가 있는 것이다. 루트 모듈에 아래와 같이 작성하면 된다.

1
2
3
4
5
6
7
import { Module } from "@nestjs/common";
import { CatsController } from "./cats/cats.controller";

@Module({
controllers: [CatsController]
})
export class AppModule {}

후기

Controller 부분은 GraphQL을 사용하게 되면 하나만 설정하게 되는 건가? 그렇다고 해도 라우팅과 컨트롤러에서 많은 매력점을 가진 프레임워크인 것 같다.

Reference

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×