Express 를 사용하여 Node.js 기반의 서버를 빠르게 구현할 수 있지만 NestJS 를 사용한다면 모듈 단위로 기능을 구분하여 구조화된 서버를 구현할 수 있습니다. 기본적으로 TypeScript 기반이며 Express 및 Fastify 위에서 동작하여 기반이 되는 라이브러리의 기능을 사용할 수 있습니다.
# NestJs Command Line Interface
npm install -g @nestjs/cli
# Creat NestJs Project
nest new folder-name
전역으로 NestJS CLI 를 설치하여 NestJS 프로젝트를 만들 수 있습니다.
더불어 개발환경에서의 핫 리로딩의 설정 또한 가능한데 현재는 별도의 설정 없이도 해당 기능이 가능합니다.
controller는 Router역활을 수행합니다.
// src/app.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('a')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('b') // GET /a/b
getHello() {
return this.appService.getHello();
}
@Post('b') // POST /a/b
getHello() {
return this.appService.postHello();
}
}
@Controller @Get @Post 와 같은 데코레이터를 사용하여 간편하게 구현할 수 있습니다. 해당 데코레이터를 NestJS가 확인하여 함수의 기능을 확장합니다.
또한 @Controller @Get @Post 의 인수값을 통해 라우터 경로를 설정가능합니다.
// Express
app.get('/', (req, res) => {
res.send('Hello World!');
});
// NestJs
// src/app.controller.ts
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();
}
}
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
위와 같이 NestJS 는 서비스를 분리하여 관리합니다. Express 에서는 하나의 라우터 및 미들웨어 내에서 요청, 비지니스, 응답 과정을 한번에 적어두는 경우가 많았는데, 서비스는 위의 비지니스 과정을 분리한 것입니다.
서비스는 트랜젝션 단위로 나누어 관리합니다.
트랜젝션 : 데이터베이스 등에서 하나의 논리적 업무를 더 이상 나눌 수 없는 최소한의 작업 묶음
이로 인하여 서비스의 재사용성이 높아지고 Express 와 달리 req res 에 대한 의존성이 없어 테스트하기에 편리합니다. (mocking 함수가 필요하지 않습니다.)
또한 공통 반환 타입( 예. API 응답 { code: ‘STATUS’, data: data } )을 Interceptor 를 사용하여 관리할 수 있습니다.
NestJS 의 여러 기능을 패키지로 다운 받아 사용이 가능합니다.
# NestJs Configuration
npm i --save @nestjs/config
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
// src/main.ts
const port = process.env.PORT || 3000
위의 패키지는 .env.[name] 과 같은 파일 추가해 환경변수별로 관리할 수도 있습니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
controllers: [AppController],
providers: [AppService, ConfigService],
})
export class AppModule {}
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppService {
constructor( private readonly ConfigService:ConfigService) {}
getHello() {
return this.ConfigService.get("SECRET")
}
}
ConfigService 를 사용하면 환경변수를 NestJs 의 모듈이 관리할 수 있습니다.
// src/app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerMiddleware } from './middleware/logger.middleware';
const getLoad = async () => {
const response = await axios.get("/비밀키요청");
return response.data;
};
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true, load: [getLoad] })],
controllers: [AppController],
providers: [AppService, ConfigService],
})
export class AppModule {}
실제 서비스에서 환경 변수를 비동기로 외부 불러와야하는 경우에도 forRoot 의 load 옵션에 함수를 넣어 사용 가능합니다.
NestJs 에서 로거를 구현 시에 pino 와 같은 라이브러리를 사용할 수도 있지만 미들웨어로 직접 만들어서 구현도 가능합니다.
// src/middlewares/logger.middleware.ts
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger('HTTP'); // 로그간의 구별을 위한 context
use(req: Request, res: Response, next: NextFunction) {
const { ip, method, originalUrl } = req;
const userAgent = req.get('user-agent') || '';
// 해당 미들웨어는 라우터보다 먼저 실행되므로 비동기로 실행
res.on('finish', () => {
const { statusCode } = res;
const contentLength = res.get('content-length');
this.logger.log(
`[${method}] ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
);
});
next();
}
}
// app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*'); // 미들웨어 등록
}
}
implements 는 부모 클레스를 상속하는 extends 와 달리 부모 클레스를 속성과 메서드를 반드시 다시 구현해 주어야 합니다. 이는 개발 시 오류를 줄여줄 수 있습니다.
// DI X
class UserService {
private repo = new UserRepository(); // 직접 생성 (강결합)
}
// DI O
class UserService {
constructor(private readonly repo: UserRepository) {} // 외부에서 주입 (약결합)
}
NextJs 는 providers 에 연결되어 있는 것들을 보고 DI(의존성 주입)을 해줍니다. 또한 NestJs 에서 클래스를 Provider 로 등록하려면 @Injectable() 을 붙입니다. 이러한 DI는 객체가 직접 의존하는 객체를 생성하지 않고, 외부에서 주입 받도록 하는 패턴으로 결합도를 낮추고 (코드 재사용 및 테스트 용이) 관리 편의성이 증가합니다.
// 직접 주입
providers: [AppService]
// 객체 기반 주입
providers: [
{
provide: AppService, // key
useClass: AppService
},
],
Provider 등록 시 클래스와 같은 경우 해당 클래스를 직접 넣어줄 수 도 있고, key 값인 provide 와 useClass useValue useFactory 와 같은 설정이 가능합니다.
이전에 Express를 한번 사용해본 경험이 있어 강의를 잘 이해할 수 있었습니다. 프론트가 아닌 백엔드에 대한 학습은 많이 해보지 않아 이번 학습이 좋은 기회라고 생각합니다. Express와 NestJS의 차이점을 중점으로 학습해두면 두 프레임워크에 대한 학습이 될 것 같습니다.