1)

$ npm i -g @nestjs/cli
$ nest new project-name

기준 폴더 02에서 명령어로 NestJS를 설치하고 프로젝트를 생성한 직후의 모습이다. Express.js 설치 시 node_modules, package-json 형제만 설치가 된 점과 대조적으로, 프로젝트 단위로 설치되며 test, eslint, prettier에 까지 구비된 점이 인상적이다.

Express.js 역시 프로젝트 단위를 설치할 수 있는 명령어가 있는지는 모르겠다.

2)

app.controller.ts

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Express.js의 router에 해당하는 controller의 모습인데 Django에서 보던 장식자 패턴이 있어서 반가웠다.

또한 엘리스 프로젝트를 진행하면서 service 및 model에 class-instance 패턴을 구현한 부분이 NestJS에서는 기본적으로 적용돼 있었다.

대략적인 흐름은 controller에서 return -> module -> main의 NestFactory라고 한다.

3)

package.json.ts

  "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  },

"refelct-metadata": 장식자 문법을 사용하게 해준다.
"rimraf": 리눅스나 맥의 rm -rf 명령어를 사용하게 해준다.
"rxjs": 비동기 및 이벤트 기반 프로그래밍을 위한 library이다.

4)

app.controller.ts

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

  @Get()
  getHello(@Req() req: Request, @Body() body, @Param() param): string {
    return this.appService.getHello();
  }
}

@Session(), @Query(), @Next()와 같이 장식자와 keyword를 사용하면 Express.js에서 req.body, req.params로 접근한 것보다 편리하게 사용할 수 있다.

'req에 종속적인 body'로 이해하는 것이 아니므로 이를 더 "객체 지향적이다."라고 이해할 수 있을까?

5)

app.controller.ts

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

app.module.ts

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})

AppController는 클래스이지만, 생성자에

this.appService = appService

를 명시해주는 과정을 거치지 않고 반환 시점에

this.appService.getHello();

를 슬쩍 끼워 넣었다. 강사님께서는 이를 "의존성을 주입한다."고 하셨고 대부분이 모듈화 되어있는 NestJS 생태계에서 각 모듈들이 연결되는 방법이라고 설명하셨다. 공식 문서에서는 wiring-up을 강조한다.

app.modele.js 파일에서는 이를 공급자/생성자(controller) 관점에서 볼 수 있는데 controller에서 소비자가 appService라는 제품을 만나게 되면 "이 제품을 어떻게 사용하지?"라는 물음을 던지고, module에 명시돼 있는 공급자 AppService(instance가 아닌 클래스로 보인다.)를 찾아과는 작업이 진행된다. 소비자-중개인-공급자의 구도를 생각해 볼 수 있겠다.

NestJS의 대부분의 클래스는 (의존성을 주입할 수 있는) 공급자로 취급될 수 있다.

https://docs.nestjs.com/providers 참조

6)

app.module.ts

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})

app.module.ts

@Module({
  imports: [CatsModule, UsersModule],
  controllers: [AppController],
  providers: [AppService],
})

명령어로 cats domain의 service를 생성하면 app.module의 imports에 저장되는데, cats.module에서 service를 exports 과정을 반드시 거쳐야 한다.

이를 cats의 공급자가 '캡슐화' 되어있다고 한다.

7)

app.module.ts

@Module({
  imports: [CatsModule],
  controllers: [AppController],
  providers: [AppService],
})

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

logger.middleware에서 기능을 정의하고, app.module에서 이를 등록해주는 sequence인데 module 장식자에는 middleware를 활용하는 용법이 없으니, module '클래스' 의 configure() method를 활용한다.

"소비자에게 LoggerMiddleware를 제공해 줄거야."를 의미하는데, 여기서는 소비자가 controller가 아닌 consumer라고 보다 분명하게 명시돼 있다. 여기서도 다시 한번, '의존성 주입이 가능한 객체'를 공급자라고 한다.

forRoutes는 MiddlewareConsumer의 MiddlewareConfigProxy에 정의되어 있다.

8)

cats.controller.ts

  @Get()
  @UseFilters(HttpExceptionFilter)
  getAllCats() {
    throw new HttpException('api is broken', 401);
    return 'all cats';
  }

http-exception.filter.ts

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const error = exception.getResponse();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      error: error,
    });
  }
}

HttpException은 Error interface를 확장한다. 에러 영역에서도 강제력이 높아진 느낌이다. HttpException 클래스의 property에 response, status, option이 있다. controller에서 exception을 발생시키면서 이를 exception.getResponse에 담고, 이를 error 식별자에 할당했다.

HttpExceptionFilter는 각각의 method 혹은 클래스에 적용할 수도 있고 아니면 전역에서 처리할 수도 있다.

HttpExceiptionFilter는 ExceptionFilter를 확장하는데 ExceptionFilter interface는 catch method를 포함한다.

9)

main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

ExceptioniFilter를 전역에 정의했다. useGlobalFilter는 INestApplicationContext를 확장하는 INestApplication interface의 method이다.

global filter로 등록된다는 말은 모든 HTTP route handler에서 사용된다는 말과 같다.

현재까지 middleware-controller-service-exception의 sequence를 확인했는데 이 흐름은 Express.js의 app.js에서도 확인할 수 있다.

10)

cats.controller.ts

  @Get(':id')
  getCat(@Param('id', ParseIntPipe, PositiveIntPipe) param: number) {
    return 'one cat';
  }

pipe를 사용하면 직렬로 정제를 거쳐 전달된 인자를 원하는 형태로 param이라는 식발자로 뽑아쓸 수 있다. valiator 기능도 갖고 있다.

https://docs.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters 참조

11)

success.interceptor.ts

@Injectable()
export class SuccessInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next.handle().pipe(
      map((data) => {
        return data;
      }),
    );
  }
}

NestInterceptor interface에 정의된 intercept method는 context와 next를 인자로 받는다. 반환값이 따르는 타입인 Observable의 경우 rxjs에서 가져오는데, JS의 비동기가 꼬이기 않게 도와주는 모듈이라고 한다. 당장 와닿진 않는데 대략 현재 시점의 route handler vs 미래 시점의 route handler로 나눠진다고 이해된다.

interceptor가 requests life-cycled에서 pre-controller와 post-request 두 시점에 나눠서 적용된다는 점이 중요하다. 이를 Aspect Oriented Programming이라고 하는 것 같다.

data에는 controller에서 return한 response가 담긴다고 이해하고 넘어가자.

https://uchanlee.dev/NestJS/overview/9/ 참조

0개의 댓글