kakasoo

Express만 하다가 Nest를 하고 느낀 점 본문

프로그래밍/NestJS

Express만 하다가 Nest를 하고 느낀 점

카카수(kakasoo) 2022. 7. 17. 12:52
반응형

 

서론

const router = express.router();

// express CRUD
router.post('/', controller.postMethod);
router.get('/', controller.getMethod);
router.put('/', controller.putMethod);
router.delete('/', controller.deleteMethod);

 

Express에서는 다음과 같이 CRUD를 만든다. 사실 간편하고 빠르게 서버 어플리케이션을 확장할 수 있다는 게 장점이다. 하지만 Express는 구조적으로 짜임새가 있지는 않다. 그래서 결국에는 차세대 프레임워크에게 자리를 물려줄 것으로 보인다. 이제부터 직접 두 프레임워크를 다루면서 느꼈던 차이점, 그리고 Express의 불편함에 대해서 말해보겠다.

 

  1. 라이프 사이클을 개발자가 직접 조정해야 한다.
  2. Route 경로가 꼬일 수 있다.
  3. TypeScript 지원 X
  4. async/await 지원 X

 

라이프 사이클 (Life Cycle)

"Express에서는 죄다 middleware라고 부른다."

 

Request부터 Response를 돌려주기까지의 라이프 사이클에 관여하는 게 middleware인 것은 맞지만, 그걸 다 middleware라고 부르는 건 지나치게 뭉뚱그리는 행위다.

 

  • Guards : passport.js와 같이 인증, 또는 Role을 부여하는 것
  • Interceptors : Request와 Response의 처음과 끝에 동작하는 종류, 예컨대 logger처럼 각 API가 동작하는 데 걸리는 ms를 측정하는 종류
  • Pipes : 값에 대한 transform, 또는 validator를 담당하는 역할
  • filters : 예외를 처리하는 역할
  • decotrators : ES2016 데코레이터, 함수를 반환하고 대상, 이름 속성 설명자를 인수로 사용하는 표현식이다. Guards, Interceptors, Pipes 등은 모두 decorator의 형태로 표현된다.

 

"Express에서 실행하는 미들웨어 순서는 개발자에게 맡겨져 있지만, 사실 그 순서는 당연히 정해진 바가 있다. 일단 로그인, 인증을 담당하는 미들웨어를 거쳐야 하고, logger와 같이 Request와 Response 각 극단을 다루는 미들웨어를 거쳐야 하며, 각 값들에 대한 검토, 잘못 들어온 값들이 body, query, parameter에 있지는 않은지 체크해줘야 한다."

 

Express 개발자들은 이런 라이프 사이클에 관한 부분을 직접 다뤄줘야 한다. 다음은 Nest.js에서의 라이프 사이클을 보자.

 

  1. Reqeust가 들어오면.
  2. Guards ( 글로벌, 컨트롤러, 루트 순서 )
  3. Interceptors ( 글로벌, 컨트롤러, 라우트 순서 )
  4. Pipes ( 글로벌, 컨트롤러, 라우트, 라우트 매개 변수 파이프 순서 )
  5. Controller // Express에서 말하는 API
  6. Service // DB에 접근하는 Business logic을 담고 있다.
  7. Interceptors ( 라우트, 컨트롤러, 글로벌 순서 )
  8. filter ( 예외 처리 )
  9. Response를 반환한다.

 

위 순서는 말했다시피 Express에서도 당연히 따라야 하는 부분들이다. 하지만 개발자가 미들웨어의 순서를 직접 조정해줘야 하는 것과 달리 Nest에서는 이 라이프 사이클이 자동으로 적용된다.

 

// Example
@UsePipes(GeneralValidationPipe) // 1
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @UsePipes(RouteSpecificPipe) // 3
  @Patch(':id')
  updateCat(
    @Body() body: UpdateCatDTO, // 2
    @Param() params: UpdateCatParams, // 2
    @Query() query: UpdateCatQuery, // 2
  ) {
    return this.catsService.updateCat(body, params, query);
  }
}

 

우리가 가지는 컨트롤러 로직이 위와 같다고 하면 결국 컨트롤러는 서비스를 호출해 updateCat이라는 메서드만 부를 뿐이다. 그러면 Service는 DB 접근 로직을 가지고 있으니, 위를 알아서 수행해줬을 것이다. 이 때, @ 기호를 통해 표현된 각 데코레이터들을 보자.

이 코드 영역만 봤을 때, 코드 순서와 무관하게 동작할 것이란 게 보장이 된다. 주석의 1,2,3 순서로 데코레이터들이 적용된다. 각 데코레이터들이 무얼 뜻하는지만 알고 있으면 미들웨어 순서를 조정하는 일은 필요없다.

 

// 첫번째 경우
@UseGuards(LocalGuards)
@UsePipes(RouteSpecificPipe)
someMethod() {}

// 두번째 경우
@UsePipes(RouteSpecificPipe)
@UseGuards(LocalGuards)
someMethod() {}

 

대충 위 두 경우의 수행 결과가 똑같다는 것이다. 내가 생각하기에 Express보다 Nest가 낫다고 할, 가장 큰 이유 중 첫 번째다.

 

Route는 알고리즘이다

 

이 내용은 잘못된 내용이므로 삭제하였습니다.

문서를 제가 오독하여 잘못 생각하였습니다. 아래 예제 코드를 작성했습니다. 제가 이 코드에 대해서 잘못 이해한 부분은 Express와 달리 Nest 내에서는 아래와 같은 경우 user/:id와 user/name을 혼동하지 않을 거라는 부분이었습니다. terada님께서 댓글로 말씀해주신 것을 보고 실제로 코드로 확인해보니 Express 때와 동일하게 동작함을 확인했습니다. 잘못된 부분을 지적해주신 terada님께 정말로 감사드립니다.

 

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

@Controller()
export class AppController {
  @Get('user/:id')
  test1() {
    return 'user id';
  }

  @Get('user')
  test2() {
    return 'user';
  }

  @Get('user/name')
  test3() {
    return 'user name';
  }
}

 

Api route is recognized as a param. #995 에서 이 문제에 대한 논의를 볼 수 있습니다.

 

 

Api route is recognized as a param. · Issue #995 · nestjs/nest

I'm submitting a... [ ] Regression [X] Bug report [ ] Feature request [ ] Documentation issue or request [ ] Support request => Please do not submit support request here, instead post your q...

github.com

 

TypeScript 지원을 안한다고?

 

Express도 TypeScript 기반으로 만들 수 있다. 그런데 Express는 TypeScript를 지원하지 않는다고 한다. 이게 무슨 말일까?

사람들이 많이 착각하고 있는 건데, Express를 TypeScript 기반으로 만들 수 있는 건, 그냥 Custom한 Type들이나 원래부터 JavaScript에 포함된 녀석들을 타입 지정할 수 있다는 뜻이다. Express 프레임워크 내에서 선언된 친구들에게는 타입을 주기 곤란한 경우가 간혹 발생한다.

 

import { Express } from "express";
declare module "express" {
  export interface Request<
    Body = any,
    Query = any,
    Params = any,
    Cookies = any,
  > extends Express.Request {
    body: Body;
    query: Query;
    params: Params;
    cookies: Cookies;
  }
}

 

위처럼 Body, Query, Params, Cookies 등이 문제가 된다. 그래서 기존의 Express.Request를 확장해줄 필요가 있다. 특히 Cookies는 cookie-parser가 멋대로 붙인 거기 때문에 더 골 때린다. JavaScript는 객체를 원하는대로 확장할 수 있기 때문에 기존의 라이브러리들이 이런 특성을 이용했다면, 개발자가 일일히 찾아가며 하나씩 허용해줘야 한다.

 

declare module "express" {
    interface Request {
      user?: User;
    }
  }
}

 

같은 의미로 Passport.js에서도 비슷한 문제가 발생할 수 있다. req.user는 passport.js에서 멋대로 붙이는 건데, TypeScript에서는 이를 허용해주지 않을 수도 있다. 다행히 요즘에는 TypeScript를 지원하는 라이브러리들이 많기 때문에 많은 부분 해소가 되었다. 하지만 Express의 개발 자체가 멈췄기 때문에 한계는 있다.

 

async/await를 지원하지 않는다

 

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

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll() {
    return await this.catsService.getAllCat(); // 이 지점
  }
}

 

TypeScript 지원을 말할 때, Express도 TypeScript가 지원되는 거 아니냐고 착각할 수 있다고 말했다. async/await도 마찬가지다. 사실 Express에서는 async/await를 지원하지 않는다. 같은 맥락에서 이게 무슨 소리냐고 할 수 있겠다. 왜냐하면 Express에서 이미 async/await를 다들 쓰고 있지 않나?

 

이 부분의 설명은 간단하다. 주석에 "이 지점" 이라고 체크한 줄을 보자. 여기서 await를 지우면 어떻게 될까? 결과는 똑같다. async/await가 있든 없든, nestjs에서는 결과를 보장한다.

 

// async/await를 지원하는 코드의 예시

const arr = [1,2,3,4,Promise.resolve(5)];

for (const a of arr) {
  if (a instanceof Promise) {
	a.then(console.log);
  } else {
    console.log(a);
  }
}

 

async/await를 지원한다는 건 이런 거다. Promise가 있든 없든 결과를 보장하는 것. 개발자가 완벽하면 되겠지만, 다들 그게 불가능하다는 건 학부 1학년생부터 이미 깨달았을 것이다.

 

그 외에도...

 

의존성 주입이나 단일 인스턴스에 대한 보장 등, Nestjs는 훌륭한 기능이 많다. Express와 달리 swagger 문서를 자동으로 달 수 있는 점, class-validator 등을 이용해서 Request의 body, query 등의 타입을 검사하는 것도, 매우 훌륭하다.

 

사실 이 모든 게 Express에서도 가능하다. 그렇지만 아마 해본 사람도 있을 것이고 안 해본 사람도 있을 것이다. 이런 걸 일일히 세팅하는 데 들어가는 수고가 좀 크다보니, 그냥 없는 셈 치고 넘어가고, 차라리 직접 코드를 쳐서 해결하자는 생각도 있었을 것이다. Nest는 이런 수고를 덜어준다. Nest는 내부에 Express를 가지고 있는 거대한 데코레이터라고 생각해도 된다. 그러니깐, Nest도 사실 Express의 확장판이라고 할 수 있겠다. 정식 확장판 같은 건 아니지만, 게임으로 치면 모드 적용이라고 비유할 수 있을까.

 

나는 Express를 이용해서 서버 쪽을 개발하는 게, 내 개발 능력의 부족인지 아니면 Express라는 프레임워크 자체의 한계인지에 대해서 고민했다. 그래서 직접 Express 코드 내부를 본 결과, 코드가 몇 줄 되지도 않을 뿐더러 그렇게 완성도 있다고 보이진 않았다. 결국에는 이 코드들을 보완할 필요가 있는데, 안타깝게도 Express 개발 팀은 Koa라는 새로운 프레임워크 개발을 시작했고, 그마저도 이제 끝난 것 같았다. 하지만 Nest는 이런 문제점들을 대거 해결했고, 3세대 프레임워크라고 불릴 만한 퀄리티를 갖추고 있다. 만약 node.js를 이용해서 개발을 하는 백엔드 개발자라면 Nest를 배우는 것을 추천한다.

 

말했다시피 Express로도 하려면 할 수 있는 것들이긴 하지만, Express만 다룰 때에는 이런 게 있다는 것도 몰랐을 것이다. 검색을 하려고 해도 키워드를 알아야 할 판인데, 뭘 검색할지도 모르는 상황이어서야 발전하기 힘들 테니깐.

반응형