왜 NestJS인가
Flutter 개발자가 백엔드를 처음 만들 때 선택지가 많습니다. Express, Fastify, Django, Spring... 그 중 NestJS를 추천하는 이유는 구조가 Flutter와 비슷하기 때문입니다.
Flutter의 Widget-State-Provider 패턴에 익숙하다면, NestJS의 Controller-Service-Module 패턴도 자연스럽게 읽힙니다. TypeScript 기반이라 Dart 경험도 살릴 수 있겠죠.
Flutter의 Widget-State-Provider 패턴에 익숙하다면, NestJS의 Controller-Service-Module 패턴도 자연스럽게 읽힙니다. TypeScript 기반이라 Dart 경험도 살릴 수 있겠죠.
프로젝트 생성
bash
# NestJS CLI 설치
npm i -g @nestjs/cli
# 프로젝트 생성
nest new my-api
# 개발 서버 실행
cd my-api
npm run start:dev
# http://localhost:3000 에서 Hello World 확인Flutter와 비교하는 핵심 구조
NestJS의 3대 구성 요소를 Flutter와 대응시켜 보겠습니다.
- Module = Flutter의 패키지/Feature 폴더. 실무에서는 관련 기능을 묶는 단위
- Controller = Flutter의 Screen/Page. HTTP 요청을 받아서 라우팅
- Service = Flutter의 Repository/UseCase. 실제 비즈니스 로직 처리
이 구조만 이해하면 NestJS의 80%는 파악한 겁니다.
- Module = Flutter의 패키지/Feature 폴더. 실무에서는 관련 기능을 묶는 단위
- Controller = Flutter의 Screen/Page. HTTP 요청을 받아서 라우팅
- Service = Flutter의 Repository/UseCase. 실제 비즈니스 로직 처리
이 구조만 이해하면 NestJS의 80%는 파악한 겁니다.
Controller: HTTP 엔드포인트 정의
TypeScript
// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users') // /users 경로 처리
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get() // GET /users
findAll() {
return this.usersService.findAll();
}
@Get(':id') // GET /users/:id
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Post() // POST /users
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}데코레이터(@Get, @Post)로 HTTP 메서드를 지정해요. Flutter의 어노테이션과 비슷한 문법이죠. constructor에서 Service를 주입받는 것도 Flutter의 Provider 주입과 같은 DI(의존성 주입) 패턴입니다.
Service: 비즈니스 로직
TypeScript
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable() // Flutter의 Provider와 같은 역할
export class UsersService {
private users = []; // 실제로는 DB 연결
findAll() {
return this.users;
}
findOne(id: string) {
const user = this.users.find(u => u.id === id);
if (!user) {
throw new NotFoundException(`User #${id} not found`);
// 자동으로 404 응답으로 변환됨
}
return user;
}
create(dto: CreateUserDto) {
const user = { id: Date.now().toString(), ...dto };
this.users.push(user);
return user;
}
}DTO로 요청 데이터 검증
TypeScript
// dto/create-user.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
}
// main.ts에서 ValidationPipe 전역 등록
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 없는 필드 자동 제거
transform: true, // 타입 자동 변환
}));DTO(Data Transfer Object)는 Flutter의 freezed 모델과 비슷한 역할예요. class-validator 데코레이터로 검증 규칙을 선언하면 잘못된 요청은 자동으로 400 에러가 돼요.
Module로 묶기
TypeScript
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // 다른 모듈에서 사용 가능
})
export class UsersModule {}Module은 Controller와 Service를 묶는 단위예요. Flutter에서 feature 폴더 안에 screen, provider, repository를 모아두는 것과 같은 개념이죠.
NestJS의 진입 장벽은 Flutter 개발자에게 특히 낮습니다. DI, 데코레이터, 모듈 패턴 모두 익숙한 개념이니까요. 다음 글에서는 TypeORM으로 실제 DB를 연결해보겠습니다.
NestJS의 진입 장벽은 Flutter 개발자에게 특히 낮습니다. DI, 데코레이터, 모듈 패턴 모두 익숙한 개념이니까요. 다음 글에서는 TypeORM으로 실제 DB를 연결해보겠습니다.