NestJS 프레임워크
이전 절에서 Node.js와 Express를 사용하여 기본적인 백엔드 애플리케이션을 구축하는 방법을 살펴보았습니다. Express는 유연하고 최소한의 기능을 제공하지만, 대규모 애플리케이션을 개발할 때는 구조화된 아키텍처와 더 많은 기능을 제공하는 프레임워크의 필요성을 느끼게 됩니다. 이때 등장하는 것이 바로 NestJS입니다.
NestJS는 Node.js 환경에서 효율적이고 확장 가능한 서버 측 애플리케이션을 구축하기 위한 진보적인 Node.js 프레임워크입니다. 타입스크립트를 기본 언어로 사용하며, Angular에서 영감을 받은 모듈식 아키텍처를 채택하여 엔터프라이즈급 애플리케이션 개발에 특히 적합합니다.
NestJS 소개 및 특징
NestJS는 다음과 같은 주요 특징을 가집니다.
- 타입스크립트 기본 지원: NestJS는 타입스크립트를 1급 시민(first-class citizen)으로 지원하여, 강력한 타입 안전성과 향상된 개발 경험을 제공합니다. 이는 대규모 프로젝트에서 코드의 유지보수성과 가독성을 크게 높입니다.
- 모듈식 아키텍처: Angular와 유사하게 모듈(Module), 컨트롤러(Controller), 프로바이더(Provider, 서비스/리포지토리 등) 개념을 사용하여 애플리케이션을 구조화합니다. 이는 코드의 응집도를 높이고, 재사용성을 촉진하며, 테스트를 용이하게 합니다.
- 의존성 주입 (Dependency Injection, DI): NestJS는 강력한 DI 컨테이너를 내장하고 있어, 컴포넌트 간의 결합도를 낮추고 테스트 가능한 코드를 작성할 수 있도록 돕습니다.
- 데코레이터 기반:
@Controller()
,@Get()
,@Injectable()
등 데코레이터를 광범위하게 사용하여 메타데이터를 추가하고, 코드를 더 선언적이고 간결하게 만듭니다. - Express 기반: 내부적으로 Express.js를 사용하며, 필요에 따라 Fastify와 같은 다른 HTTP 프레임워크로 쉽게 전환할 수 있습니다. Express의 미들웨어와 기능을 그대로 활용할 수 있습니다.
- CLI (Command Line Interface): 강력한 CLI 도구를 제공하여 프로젝트 생성, 모듈/컨트롤러/서비스 생성 등 반복적인 작업을 자동화하여 개발 생산성을 높입니다.
- 문서화: 매우 잘 정리된 공식 문서를 제공하여 학습 및 문제 해결에 용이합니다.
NestJS 프로젝트 시작하기
NestJS 프로젝트를 시작하는 가장 쉬운 방법은 Nest CLI를 사용하는 것입니다.
-
Nest CLI 설치
npm install -g @nestjs/cli
-
새 프로젝트 생성
nest new my-nestjs-app cd my-nestjs-app
이 명령어는 기본 NestJS 프로젝트 구조와 필요한 모든 의존성을 설치합니다.
-
개발 서버 실행
npm run start:dev
서버가
http://localhost:3000
에서 실행됩니다. 기본적으로/
경로로 접속하면 "Hello World!"를 반환합니다.
NestJS의 핵심 구성 요소와 타입스크립트
NestJS는 모듈, 컨트롤러, 프로바이더(서비스)의 세 가지 핵심 구성 요소로 애플리케이션을 구조화합니다.
모듈 (Modules)
모듈은 애플리케이션의 구조를 조직화하는 기본 단위입니다. 관련 컨트롤러와 프로바이더를 그룹화합니다. 모든 NestJS 애플리케이션은 최소한 하나의 루트 모듈(일반적으로 AppModule
)을 가집니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [], // 이 모듈에서 사용할 다른 모듈들을 임포트합니다.
controllers: [AppController], // 이 모듈에 속한 컨트롤러들을 정의합니다.
providers: [AppService], // 이 모듈에 속한 프로바이더(서비스, 리포지토리 등)들을 정의합니다.
})
export class AppModule {}
@Module()
데코레이터는 클래스를 NestJS 모듈로 선언합니다.
컨트롤러 (Controllers)
컨트롤러는 들어오는 요청(Request)을 처리하고 응답(Response)을 반환하는 역할을 합니다. 특정 경로(Route)에 대한 요청을 담당하며, 요청의 유효성을 검사하고, 서비스 계층으로 작업을 위임합니다.
// src/app.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { AppService } from './app.service';
// DTO (Data Transfer Object) 정의: 요청 본문의 타입을 명시합니다.
interface CreateUserDto {
name: string;
email: string;
}
@Controller('users') // '/users' 경로에 대한 요청을 처리합니다.
export class AppController {
constructor(private readonly appService: AppService) {} // 의존성 주입 (AppService)
@Get() // GET /users
getHello(): string {
return this.appService.getHello();
}
@Get(':id') // GET /users/:id
// @Param() 데코레이터는 URL 파라미터를 가져오고, 타입스크립트가 타입을 추론합니다.
getUserById(@Param('id') id: string): string {
return `User ID: ${id}`;
}
@Post() // POST /users
// @Body() 데코레이터는 요청 본문을 가져오고, CreateUserDto 타입을 명시하여 타입 안정성을 확보합니다.
createUser(@Body() createUserDto: CreateUserDto): string {
console.log('Received new user:', createUserDto);
return `User ${createUserDto.name} (${createUserDto.email}) created!`;
}
}
@Controller()
, @Get()
, @Post()
, @Body()
, @Param()
등 다양한 데코레이터와 타입스크립트의 인터페이스를 활용하여 라우팅과 요청 본문의 타입을 명확하게 정의할 수 있습니다.
프로바이더 (Providers)
프로바이더는 NestJS의 핵심 개념 중 하나입니다. 서비스(Service), 리포지토리(Repository), 팩토리(Factory) 등 애플리케이션의 비즈니스 로직을 포함하는 모든 클래스는 프로바이더가 될 수 있습니다. 프로바이더는 의존성 주입 시스템을 통해 다른 컴포넌트(컨트롤러, 다른 서비스 등)에 주입될 수 있습니다.
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리될 수 있음을 나타냅니다.
export class AppService {
getHello(): string {
return 'Hello World from AppService!';
}
// 예시: 사용자 데이터 처리 로직 (실제로는 데이터베이스 연동)
createUser(name: string, email: string): { id: number; name: string; email: string } {
const newUser = { id: Math.floor(Math.random() * 1000), name, email };
console.log('User created in service:', newUser);
return newUser;
}
}
@Injectable()
데코레이터는 NestJS가 이 클래스의 인스턴스를 생성하고 필요할 때 주입할 수 있도록 합니다.
NestJS의 고급 기능과 타입스크립트
NestJS는 엔터프라이즈급 애플리케이션 개발을 위한 다양한 고급 기능을 제공하며, 이들 역시 타입스크립트와 잘 통합됩니다.
파이프 (Pipes)
파이프는 들어오는 요청 데이터를 변환(transform)하거나 유효성 검사(validate)하는 데 사용됩니다. 컨트롤러 핸들러가 실행되기 전에 데이터를 처리합니다.
// src/pipes/parse-int.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed: Parameter is not an integer.');
}
return val;
}
}
// src/app.controller.ts (사용 예시)
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
// ...
@Controller('users')
export class AppController {
// ...
@Get(':id')
// @Param('id', ParseIntPipe)를 사용하여 id 파라미터가 숫자로 변환되고 유효성 검사됩니다.
getUserById(@Param('id', ParseIntPipe) id: number): string {
// 이제 id는 number 타입임을 보장할 수 있습니다.
return `User ID: ${id}`;
}
}
PipeTransform<T, R>
인터페이스를 구현하여 파이프의 입력(T
)과 출력(R
) 타입을 명확히 정의할 수 있습니다.
가드 (Guards)
가드는 특정 라우트 핸들러가 실행되기 전에 권한 부여(authorization) 로직을 처리하는 데 사용됩니다.
// src/guards/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// 실제로는 JWT 토큰 검증, 세션 확인 등의 로직이 들어갑니다.
const hasAuthHeader = request.headers.authorization ? true : false;
console.log('AuthGuard is running. Has Auth Header:', hasAuthHeader);
return hasAuthHeader; // 인증 성공 시 true, 실패 시 false 반환
}
}
// src/app.controller.ts (사용 예시)
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './guards/auth.guard';
// ...
@Controller('admin')
@UseGuards(AuthGuard) // 이 컨트롤러의 모든 라우트에 AuthGuard 적용
export class AdminController {
@Get('dashboard') // GET /admin/dashboard
getDashboard(): string {
return 'Welcome to the Admin Dashboard!';
}
}
CanActivate
인터페이스를 구현하여 가드의 동작을 정의하고, @UseGuards()
데코레이터를 사용하여 컨트롤러나 특정 라우트에 적용할 수 있습니다.
NestJS와 타입스크립트의 시너지
NestJS는 타입스크립트를 기본으로 채택함으로써 다음과 같은 강력한 이점을 제공합니다.
- 설계 일관성: 타입스크립트의 인터페이스, 클래스, 데코레이터 등을 활용하여 백엔드 애플리케이션의 아키텍처를 일관되고 예측 가능하게 설계할 수 있습니다.
- 컴파일 시점 오류 감지: 런타임에 발생할 수 있는 타입 관련 오류를 개발 단계에서 미리 잡아내어 디버깅 시간을 단축하고 코드의 안정성을 높입니다.
- 강력한 IDE 지원: 타입 정보 덕분에 자동 완성, 코드 탐색, 리팩토링 기능이 매우 강력해져 개발 생산성이 크게 향상됩니다.
- 문서화 효과: 타입 정의 자체가 코드의 의도와 데이터 구조를 명확하게 문서화하는 역할을 하여, 팀원 간의 협업을 원활하게 합니다.
- 확장성 및 유지보수성: 모듈, 컨트롤러, 프로바이더로 구분된 명확한 구조와 의존성 주입은 애플리케이션의 확장과 장기적인 유지보수를 용이하게 합니다.
결론
NestJS는 Node.js 환경에서 견고하고 확장 가능한 백엔드 애플리케이션을 구축하기 위한 탁월한 프레임워크입니다. 타입스크립트를 기본 언어로 사용하고, 모듈식 아키텍처, 의존성 주입, 데코레이터 기반의 간결한 문법을 제공하여 개발 생산성과 코드 품질을 동시에 높입니다. Express와 같은 경량 프레임워크가 소규모 프로젝트에 적합하다면, NestJS는 대규모 엔터프라이즈급 애플리케이션 개발에 강력한 구조와 도구를 제공하여 복잡한 백엔드 로직을 효율적으로 관리할 수 있게 해줍니다.