TypeScript ๊ธฐ๋ฐ ์ํฐํ๋ผ์ด์ฆ Node.js ๋ฐฑ์๋ ํ๋ ์์ํฌ ์๋ฒฝ ๊ฐ์ด๋: ํ์ ์ฌ์ ์ค์น ์๊ฑด, ๋ชจ๋ยท์ปจํธ๋กค๋ฌยท์๋น์ค ์ํคํ
์ฒ ๋ฐ ์์กด์ฑ ์ฃผ์
(DI), ์์ฒญ ์ฒ๋ฆฌ ์๋ช
์ฃผ๊ธฐ(MiddlewareยทGuardยทInterceptorยทPipeยทFilter), DTO ์ ํจ์ฑ ๊ฒ์ฆ, TypeORM/Prisma ์ฐ๋, JWT ์ธ์ฆ ๋ฐ ํ๋ก๋์
์ด์ ์ฒดํฌ๋ฆฌ์คํธ
## 1. ํ์ ์ฌ์ ์ค์น ์๊ฑด & ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ (Prerequisites)
NestJS๋ TypeScript๋ฅผ ๊ธฐ๋ณธ ์ธ์ด๋ก ์ฑํํ ์ง๋ณด๋ Node.js ์ํฐํ๋ผ์ด์ฆ ๋ฐฑ์๋ ํ๋ ์์ํฌ์
๋๋ค. Spring Boot์ ์ ์ฌํ ๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ(OOP), ํจ์ํ ํ๋ก๊ทธ๋๋ฐ(FP), ํจ์ ๋ฐ์ํ ํ๋ก๊ทธ๋๋ฐ(FRP) ์์์ ๊ฐ๋ ฅํ ์์กด์ฑ ์ฃผ์
(DI) ์์คํ
์ ์ ๊ณตํฉ๋๋ค.
### ๐ข 1. Node.js ๋ฐํ์ ๋ฒ์ & ๋ฒ์ ๊ด๋ฆฌ์
NestJS๋ **Node.js 18.x, 20.x, 22.x LTS ์ด์** ํ๊ฒฝ์ ํ์๋ก ํฉ๋๋ค.
```bash
# 1. Node.js ๋ฒ์ ํ์ธ (v18.0.0 ์ด์ ํ์, ๊ถ์ฅ: v20.x ๋๋ v22.x LTS)
node -v
npm -v
# 2. Node ๋ฒ์ ๊ด๋ฆฌ์(fnm ๋๋ nvm)๋ฅผ ํตํ ์ต์ LTS ์ค์น (Windows / macOS / Linux)
# fnm ์ฌ์ฉ ์ (์ด๊ณ ์ ๊ถ์ฅ)
fnm install --lts
fnm use lts-latest
# nvm ์ฌ์ฉ ์
nvm install --lts
nvm use --lts
```
### ๐ ๏ธ 2. NestJS CLI ์ค์น ๋ฐ ์ ํ๋ก์ ํธ ์์ฑ
๊ณต์ Nest CLI๋ฅผ ์ ์ญ์ผ๋ก ์ค์นํ๊ฑฐ๋, `npx`๋ก ๋ฐ๋ก ์ค์บํด๋ฉํ ์ ์์ต๋๋ค:
```bash
# ์ต์
A: Nest CLI ์ ์ญ ์ค์น (๊ถ์ฅ)
npm install -g @nestjs/cli
nest --version
# ์ ํ๋ก์ ํธ ์ค์บํด๋ฉ ์์ฑ (ํจํค์ง ๋งค๋์ ๋ npm/pnpm/yarn ์ค ์ ํ)
nest new my-nest-app --package-manager npm
cd my-nest-app
# ์ต์
B: CLI ์ ์ญ ์ค์น ์์ด npx๋ก ์ฆ์ ์์ฑ
npx @nestjs/cli new my-nest-app --package-manager npm
cd my-nest-app
```
### โ๏ธ 3. ํ์ TypeScript ์ปดํ์ผ๋ฌ ์ค์ (`tsconfig.json`)
NestJS๋ ๋ฐ์ฝ๋ ์ดํฐ(`@Controller`, `@Injectable` ๋ฑ)์ ๋ฆฌํ๋ ์
๋ฉํ๋ฐ์ดํฐ๋ฅผ ํต์ฌ์ผ๋ก ์ฌ์ฉํ๋ฏ๋ก, `tsconfig.json`์ ๋ค์ ์ต์
์ด ๋ฐ๋์ ํ์ฑํ๋์ด ์์ด์ผ ํฉ๋๋ค:
```json
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}
```
### ๐ฆ 4. ํ์ ์์กด์ฑ ํจํค์ง ์ค์น
๊ฒ์ฆ ํ์ดํ ๋ฐ ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ์ ํ์์ ์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํฉ๋๋ค:
```bash
# DTO ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๊ฐ์ฒด ๋ณํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
npm install class-validator class-transformer
# ํ๊ฒฝ๋ณ์(.env) ์ค์ ๋ชจ๋
npm install @nestjs/config
# ๊ฐ๋ฐ ์๋ฒ ์คํ (์ฝ๋ ๋ณ๊ฒฝ ์ ์๋ ํซ ๋ฆฌ๋ก๋)
npm run start:dev
```
---
## 2. NestJS ํต์ฌ ๊ฐ๋
& ์์ฒญ ์ฒ๋ฆฌ ์๋ช
์ฃผ๊ธฐ (Mental Model)
NestJS๋ก ๋ค์ด์ค๋ ๋ชจ๋ HTTP ์์ฒญ์ ์ผ๊ด๋๊ณ ์ฒด๊ณ์ ์ธ ํ์ดํ๋ผ์ธ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ ์ฒ๋ฆฌ๋ฉ๋๋ค.
### ๐ HTTP ์์ฒญ ์ฒ๋ฆฌ ์๋ช
์ฃผ๊ธฐ (Request Lifecycle)
```text
[HTTP ํด๋ผ์ด์ธํธ ์์ฒญ]
โ
โผ
1. Middleware (๋ก๊น
, ์ธ์
, CORS, Body ํ์ฑ ๋ฑ ์ ์ญ/๋ผ์ฐํธ ๋ฏธ๋ค์จ์ด)
โ
โผ
2. Guards (์ธ์ฆ ํ ํฐ ๊ฒ์ฆ, Role/๊ถํ ์ธ๊ฐ ์ฒดํฌ: canActivate())
โ
โผ
3. Interceptors (Pre-Controller: ํธ๋ค๋ฌ ์ง์
์ ์๊ฐ ์ธก์ , ์์ฒญ ๋ณํ)
โ
โผ
4. Pipes (DTO ์คํค๋ง ๊ฒ์ฆ: class-validator, ํ์
ํ๋ณํ: ParseIntPipe)
โ
โผ
5. Controller (ํธ๋ค๋ฌ ํจ์ ๋ผ์ฐํ
: @Get, @Post, ์์ฒญ ๊ฐ์ฒด ์ถ์ถ)
โ
โผ
6. Service / Provider (๋น์ฆ๋์ค ๋ก์ง ์ฐ์ฐ, DB ์ฟผ๋ฆฌ, ์ธ๋ถ API ํธ์ถ)
โ
โผ
7. Interceptors (Post-Controller: RxJS pipe๋ก ์๋ต ๊ตฌ์กฐ ํ์คํ, ์บ์ฑ)
โ
โผ
8. Exception Filters (์ฒ๋ฆฌ ์ค ๋ฐ์ํ ์์ธ๋ฅผ ์ผ๊ด๋ JSON ์๋ฌ ํ์์ผ๋ก ๋ณํ)
โ
โผ
[ํด๋ผ์ด์ธํธ๋ก HTTP ์๋ต ์ ์ก]
```
### ๐งฉ 3๋ ํต์ฌ ์ํคํ
์ฒ ๊ตฌ์ฑ์์
1. **Modules (`@Module`)**: ์ ํ๋ฆฌ์ผ์ด์
๊ตฌ์กฐ๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒฝ๊ณ ๋จ์. Controller์ Provider๋ฅผ ๋ฌถ๊ณ , ๋ค๋ฅธ ๋ชจ๋๊ณผ ๊ฒฐํฉํฉ๋๋ค.
2. **Controllers (`@Controller`)**: ๋ค์ด์ค๋ ํด๋ผ์ด์ธํธ HTTP ์์ฒญ์ ์์ ํ๊ณ , ๊ฒฝ๋ก ๋งคํ ํ ์ ์ ํ Service๋ก ์์ํฉ๋๋ค.
3. **Providers / Services (`@Injectable`)**: ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๋ฉฐ, NestJS IoC(์ ์ด์ ์ญ์ ) ์ปจํ
์ด๋์ ์ํด ์๋์ผ๋ก ์์ฑ์ ์ฃผ์
(DI)๋ฉ๋๋ค.
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] ํฌ๋ก์๋: ์ต์ ๋์ ์์
Nest CLI์ ๋ณต์กํ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์์ด, ๋จ์ผ ํ์ผ ์์ค์ ์ต์ ์ฝ๋๋ก NestJS์ ๊ตฌ๋ ์๋ฆฌ๋ฅผ ์ฆ์ ํ์
ํ ์ ์๋ ์์ ์
๋๋ค.
### ๐ ํ์ผ ์์ฑ: `src/main-standalone.ts`
```typescript
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { Module, Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
// 1. Controller ์ ์: HTTP ์์ฒญ์ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ
@Controller('api')
class HelloController {
@Get('hello')
getHello(): { message: string; timestamp: string } {
return {
message: 'Hello, NestJS World!',
timestamp: new Date().toISOString(),
};
}
@Get('users/:id')
getUser(@Param('id', ParseIntPipe) id: number): { userId: number; status: string } {
return {
userId: id,
status: 'active',
};
}
}
// 2. Module ์ ์: ์ปจํธ๋กค๋ฌ์ ์์กด์ฑ ํ๋ก๋ฐ์ด๋๋ฅผ ๋ฌถ๋ ์ปจํ
์ด๋
@Module({
controllers: [HelloController],
})
class HelloAppModule {}
// 3. ๋ถํธ์คํธ๋ฉ ํจ์: Nest ์ ํ๋ฆฌ์ผ์ด์
์ธ์คํด์ค ์์ฑ ๋ฐ ํฌํธ ๋ฆฌ์ค๋
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(HelloAppModule);
const PORT = 3000;
await app.listen(PORT);
console.log(`๐ NestJS ์๋ฒ๊ฐ ๊ตฌ๋๋์์ต๋๋ค: http://localhost:${PORT}/api/hello`);
}
bootstrap();
```
### ๐ป ์คํ ๋ฐ ๋์ ํ์ธ
```bash
# ts-node๋ก ์ง์ ์คํ
npx ts-node src/main-standalone.ts
# ์ฝ์ ์ถ๋ ฅ ํ์ธ:
# ๐ NestJS ์๋ฒ๊ฐ ๊ตฌ๋๋์์ต๋๋ค: http://localhost:3000/api/hello
# ๋ค๋ฅธ ํฐ๋ฏธ๋์์ curl ์์ฒญ ํ
์คํธ:
curl http://localhost:3000/api/hello
# ์๋ต ๊ฒฐ๊ณผ:
# {"message":"Hello, NestJS World!","timestamp":"2026-10-08T11:55:00.000Z"}
curl http://localhost:3000/api/users/42
# ์๋ต ๊ฒฐ๊ณผ:
# {"userId":42,"status":"active"}
```
---
## 4. [์ด๋ณด์ 2๋จ๊ณ] ํต์ฌ ๊ธฐ๋ฅ & ๋ชจ๋ ์ํคํ
์ฒ
์ค๋ฌด NestJS ํ๋ก์ ํธ์ ๊ธฐ๋ณธ ํ์ค์ธ ์ปจํธ๋กค๋ฌ, ์๋น์ค, DTO ๊ฒ์ฆ ํ์ดํ ๋ฐ ๋ชจ๋ ์์คํ
๊ตฌ์กฐ์
๋๋ค.
### ๐น๏ธ 1. DTO & Validation Pipe (์
๋ ฅ๊ฐ ์ ํจ์ฑ ๊ฒ์ฆ)
ํด๋ผ์ด์ธํธ์ ์์ฒญ ๋ฐ๋(Body)๋ฅผ ์์ ํ๊ฒ ๊ฒ์ฆํ๋ DTO(Data Transfer Object) ํด๋์ค์
๋๋ค.
```typescript
// src/posts/dto/create-post.dto.ts
import { IsString, IsNotEmpty, MinLength, MaxLength, IsOptional, IsBoolean } from 'class-validator';
export class CreatePostDto {
@IsString({ message: '์ ๋ชฉ์ ๋ฌธ์์ด์ด์ด์ผ ํฉ๋๋ค.' })
@IsNotEmpty({ message: '์ ๋ชฉ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.' })
@MinLength(2, { message: '์ ๋ชฉ์ ์ต์ 2์ ์ด์์ด์ด์ผ ํฉ๋๋ค.' })
@MaxLength(100, { message: '์ ๋ชฉ์ ์ต๋ 100์๊น์ง ๊ฐ๋ฅํฉ๋๋ค.' })
readonly title: string;
@IsString({ message: '๋ด์ฉ์ ๋ฌธ์์ด์ด์ด์ผ ํฉ๋๋ค.' })
@IsNotEmpty({ message: '๋ด์ฉ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.' })
readonly content: string;
@IsBoolean()
@IsOptional()
readonly isPublished?: boolean = false;
}
```
### โ๏ธ 2. Service & ์์กด์ฑ ์ฃผ์
(Dependency Injection)
`@Injectable()` ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ถ์ ํด๋์ค๋ Nest IoC ์ปจํ
์ด๋๊ฐ ์ธ์คํด์ค ์๋ช
์ฃผ๊ธฐ(์ฑ๊ธํค ๊ธฐ๋ณธ๊ฐ)๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
```typescript
// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
export interface PostItem {
id: number;
title: string;
content: string;
isPublished: boolean;
createdAt: Date;
}
@Injectable()
export class PostsService {
private posts: PostItem[] = [
{
id: 1,
title: 'NestJS ์์ํ๊ธฐ',
content: 'NestJS ์ํคํ
์ฒ์ DI ์์คํ
์ตํ๊ธฐ',
isPublished: true,
createdAt: new Date(),
},
];
private idCounter = 2;
findAll(): PostItem[] {
return this.posts;
}
findOne(id: number): PostItem {
const post = this.posts.find((p) => p.id === id);
if (!post) {
throw new NotFoundException(`ID๊ฐ ${id}์ธ ๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.`);
}
return post;
}
create(dto: CreatePostDto): PostItem {
const newPost: PostItem = {
id: this.idCounter++,
title: dto.title,
content: dto.content,
isPublished: dto.isPublished ?? false,
createdAt: new Date(),
};
this.posts.push(newPost);
return newPost;
}
remove(id: number): { success: boolean; deletedId: number } {
this.findOne(id); // ์์ผ๋ฉด NotFoundException ๋ฐ์
this.posts = this.posts.filter((p) => p.id !== id);
return { success: true, deletedId: id };
}
}
```
### ๐ 3. Controller & HTTP ๋ผ์ฐํ
๊ฒฝ๋ก ํ๋ผ๋ฏธํฐ(`@Param`), ์ฟผ๋ฆฌ์คํธ๋ง(`@Query`), ๋ณธ๋ฌธ(`@Body`), ์ํ์ฝ๋(`@HttpCode`)๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
```typescript
// src/posts/posts.controller.ts
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
ParseIntPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { PostsService, PostItem } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
@Controller('posts')
export class PostsController {
// ์์ฑ์ ์ฃผ์
(Constructor Injection): IoC ์ปจํ
์ด๋๊ฐ PostsService ์ฑ๊ธํค ์ฃผ์
constructor(private readonly postsService: PostsService) {}
@Get()
getAllPosts(): PostItem[] {
return this.postsService.findAll();
}
@Get(':id')
getPostById(@Param('id', ParseIntPipe) id: number): PostItem {
return this.postsService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
createPost(@Body() dto: CreatePostDto): PostItem {
return this.postsService.create(dto);
}
@Delete(':id')
deletePost(@Param('id', ParseIntPipe) id: number): { success: boolean; deletedId: number } {
return this.postsService.remove(id);
}
}
```
### ๐ฆ 4. ๋ชจ๋ ๋ถ๋ฆฌ (`@Module`) ๋ฐ ์ ์ญ ํ์ดํ ๋ฑ๋ก
๊ธฐ๋ฅ ๋จ์ ๋ชจ๋๋ก ๋ถ๋ฆฌํ๊ณ `AppModule`์ ํตํฉํฉ๋๋ค.
```typescript
// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService], // ๋ค๋ฅธ ๋ชจ๋์์ PostsService๋ฅผ ์ฃผ์
๋ฐ์ ์ ์๋๋ก ๋
ธ์ถ
})
export class PostsModule {}
```
```typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [PostsModule],
})
export class AppModule {}
```
```typescript
// src/main.ts (์ง์
์ : ์ ์ญ ValidationPipe ํ์ฑํ)
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// ์ ์ญ ์ ํจ์ฑ ๊ฒ์ฌ ํ์ดํ ๋ฑ๋ก (๋งค์ฐ ์ค์)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // DTO์ ์ ์๋์ง ์์ ์์์ ์์ฑ์ ์๋ ์ ๊ฑฐ
forbidNonWhitelisted: true, // DTO์ ์ ์๋์ง ์์ ํ๋ ์ ๋ฌ ์ 400 Bad Request ์๋ฌ ๋ฐํ
transform: true, // ์์ฒญ ํ์ด๋ก๋๋ฅผ DTO ํด๋์ค ์ธ์คํด์ค๋ก ์๋ ๋ณํ
}),
);
await app.listen(3000);
console.log('Server running on http://localhost:3000');
}
bootstrap();
```
---
## 5. [์ด๋ณด์ 3๋จ๊ณ] ์ค๋ฌด ์์ฉ ํจํด & ๋ฏธ๋ค์จ์ด ํ์ดํ๋ผ์ธ
์ค๋ฌด ํ๋ก๋์
ํ๊ฒฝ์์ ํ์์ ์ผ๋ก ์ฌ์ฉ๋๋ ์ค์ ๋ชจ๋, ์์ธ ํํฐ, ์ธํฐ์
ํฐ, ์ธ์ฆ ๊ฐ๋, Swagger ๋ฌธ์ํ ํจํด์
๋๋ค.
### ๐ 1. ConfigModule & ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ (`@nestjs/config`)
```bash
npm install @nestjs/config
```
```typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // ๋ชจ๋ ๋ชจ๋์์ ConfigService ์ฃผ์
๊ฐ๋ฅ
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
],
})
export class AppModule {}
```
```typescript
// ์๋น์ค์์ ํ๊ฒฝ๋ณ์ ์ฌ์ฉ ์์
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DatabaseConfigService {
constructor(private readonly configService: ConfigService) {}
get databaseUrl(): string {
return this.configService.get<string>('DATABASE_URL', 'localhost:5432');
}
}
```
### ๐ก๏ธ 2. ์ ์ญ ์์ธ ํํฐ (HttpExceptionFilter)
๋ฐฑ์๋ ์ ์ฒด์์ ๋ฐ์ํ๋ ์๋ฌ ์๋ต ํฌ๋งท์ ์ผ๊ด๋ JSON ์คํค๋ง๋ก ํ์คํํฉ๋๋ค.
```typescript
// src/common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error: typeof message === 'object' ? message : { message },
};
this.logger.error(`[${request.method}] ${request.url} - Status: ${status}`);
response.status(status).json(errorResponse);
}
}
```
### โฑ๏ธ 3. ๋ก๊น
& ์คํ์๊ฐ ์ธก์ ์ธํฐ์
ํฐ (LoggingInterceptor)
์์ฒญ๊ณผ ์๋ต์ ์คํ ์๊ฐ์ RxJS์ `tap` ์ฐ์ฐ์๋ก ๊ณ์ธกํฉ๋๋ค.
```typescript
// src/common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const delay = Date.now() - now;
this.logger.log(`[${method}] ${url} ${response.statusCode} +${delay}ms`);
}),
);
}
}
```
### ๐ 4. ์ธ์ฆ & ์ธ๊ฐ ๊ฐ๋ (Auth Guard)
์์ฒญ ํค๋์ Authorization ํ ํฐ์ ๊ฒ์ฆํ์ฌ ํธ๋ค๋ฌ ์ ๊ทผ์ ์ ์ดํฉ๋๋ค.
```typescript
// src/common/guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class SimpleAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('์ธ์ฆ ํ ํฐ์ด ๋๋ฝ๋์๊ฑฐ๋ ์ ํจํ์ง ์์ต๋๋ค.');
}
const token = authHeader.split(' ')[1];
// ํ ํฐ ๊ฒ์ฆ ๋ก์ง (์ค๋ฌด: jwtService.verify(token))
if (token !== 'secret-lucky-token') {
throw new UnauthorizedException('์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค.');
}
// ๊ฒ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ request ๊ฐ์ฒด์ ์ฃผ์
request.user = { userId: 1, username: 'lucky_developer' };
return true;
}
}
// ์ปจํธ๋กค๋ฌ ์ ์ฉ ์์:
// @UseGuards(SimpleAuthGuard)
// @Get('protected')
// getProtectedData(@Req() req) { return req.user; }
```
### ๐ 5. Swagger (OpenAPI) ๋ฌธ์ ์๋ ์์ฑ
```bash
npm install @nestjs/swagger
```
```typescript
// src/main.ts ์์ Swagger ์ค์ ๋ฑ๋ก
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('LuckyTechWorld API')
.setDescription('NestJS ๊ณ ์ฑ๋ฅ ์ํฐํ๋ผ์ด์ฆ REST API ๋ช
์ธ์')
.setVersion('1.0.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
await app.listen(3000);
console.log('Swagger UI: http://localhost:3000/api/docs');
}
bootstrap();
```
---
## 6. ์ค๋ฌด ์ฃผ์์ฌํญ & ํ๋ก๋์
์ฒดํฌ๋ฆฌ์คํธ
### โ ๏ธ 1. ์ด๋ณด์๊ฐ ๊ฐ์ฅ ํํ ๊ฒช๋ ์ค์ & ํด๊ฒฐ๋ฒ
| ๋ฒํธ | ํํ ์๋ฌ ๋ฐ ์ฆ์ | ๊ทผ๋ณธ ์์ธ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
| :---: | :--- | :--- | :--- |
| **1** | `Nest can't resolve dependencies of the XService (?)` | ์์กดํ๋ Service๊ฐ ์ํ ๋ชจ๋์์ `exports`ํ์ง ์์๊ฑฐ๋, ๋์ ๋ชจ๋์ `imports` ๋ฐฐ์ด์ ์ถ๊ฐํ์ง ์์ | ๊ณต๊ธ์ ๋ชจ๋์ `@Module({ exports: [XService] })` ํ์ธ ํ ์๋น์ ๋ชจ๋์ `imports: [XModule]` ์ถ๊ฐ |
| **2** | ์ํ ์ฐธ์กฐ ์๋ฌ (`A โ B โ A`) | ๋ ๋ชจ๋ ๋๋ ์๋น์ค๊ฐ ์๋ก๋ฅผ ์ง์ ์ฃผ์
ํ๊ณ ์์ | ์์ฑ์ ์ฃผ์
์ `@Inject(forwardRef(() => OtherService))` ๋ฐ `imports: [forwardRef(() => OtherModule)]` ์ฌ์ฉ |
| **3** | DTO ๊ฒ์ฆ ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์ ํ ๋์ํ์ง ์์ | `main.ts`์ `app.useGlobalPipes(new ValidationPipe())` ๋ฏธ๋ฑ๋ก | `main.ts` ๋ถํธ์คํธ๋ฉ ํจ์ ๋ด ์ ์ญ `ValidationPipe` ์ถ๊ฐ |
| **4** | ๋ฐ์ฝ๋ ์ดํฐ ๋ฉํ๋ฐ์ดํฐ ์๋ฌ (`Unable to resolve signature...`) | `tsconfig.json` ์ปดํ์ผ๋ฌ ํ๋๊ทธ ๋๋ฝ | `experimentalDecorators: true`, `emitDecoratorMetadata: true` ์ค์ ํ์ธ |
### ๐ 2. ํ๋ก๋์
์ฑ๋ฅ & ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
1. **Fastify ์ด๋ํฐ ์ ํ (์ด๊ณ ์ ์ฑ๋ฅ ํ์ ์)**:
๊ธฐ๋ณธ Express ์ด๋ํฐ ๋์ Fastify๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋น ์ฒ๋ฆฌ๋(RPS)์ด ์ต๋ 2๋ฐฐ๊น์ง ํฅ์๋ฉ๋๋ค.
```bash
npm install @nestjs/platform-fastify
```
```typescript
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
```
2. **๋ณด์ ํค๋ & CORS**:
```bash
npm install helmet
```
```typescript
import helmet from 'helmet';
app.use(helmet());
app.enableCors({ origin: ['https://luckytech.world'], credentials: true });
```
3. **๋๋์ค/๋จ์ฉ ๋ฐฉ์ง Rate Limiting**:
```bash
npm install @nestjs/throttler
```
4. **Graceful Shutdown (์ฐ์ํ ์ข
๋ฃ ๋ณด์ฅ)**:
Kubernetes ์ปจํ
์ด๋ ๋กค๋ง ๋ฐฐํฌ ์ ์งํ ์ค์ธ ์์ฒญ์ ์ ์ค์ ๋ฐฉ์งํฉ๋๋ค:
```typescript
// main.ts
app.enableShutdownHooks();
```
### โก 3. Nest CLI ํต์ฌ ๋จ์ถ ๋ช
๋ น์ด ๋ชจ์
| ์์
๋ชฉ์ | ์ ์ฒด ๋ช
๋ น์ด | ๋จ์ถ ๋ช
๋ น์ด | ์๋ ์์ฑ ๊ฒฐ๊ณผ๋ฌผ |
| :--- | :--- | :--- | :--- |
| **ํ๋ก์ ํธ ์์ฑ** | `nest new <์ด๋ฆ>` | `nest new <์ด๋ฆ>` | ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค์บํด๋ฉ |
| **CRUD ์ ์ฒด ๋ฆฌ์์ค** | `nest generate resource <์ด๋ฆ>` | `nest g res <์ด๋ฆ>` | Controller + Service + Module + DTO + Entity ์ผ๊ด ์์ฑ |
| **๋ชจ๋ ์์ฑ** | `nest generate module <์ด๋ฆ>` | `nest g mo <์ด๋ฆ>` | `xxx.module.ts` ์์ฑ ๋ฐ AppModule ๋ฑ๋ก |
| **์ปจํธ๋กค๋ฌ ์์ฑ** | `nest generate controller <์ด๋ฆ>` | `nest g co <์ด๋ฆ>` | `xxx.controller.ts` ์์ฑ ๋ฐ Module ๋ฑ๋ก |
| **์๋น์ค ์์ฑ** | `nest generate service <์ด๋ฆ>` | `nest g s <์ด๋ฆ>` | `xxx.service.ts` ์์ฑ ๋ฐ Module ๋ฑ๋ก |
| **๊ฐ๋ ์์ฑ** | `nest generate guard <์ด๋ฆ>` | `nest g gu <์ด๋ฆ>` | `xxx.guard.ts` ์์ฑ |
| **์ธํฐ์
ํฐ ์์ฑ** | `nest generate interceptor <์ด๋ฆ>` | `nest g in <์ด๋ฆ>` | `xxx.interceptor.ts` ์์ฑ |
| **์์ธ ํํฐ ์์ฑ** | `nest generate filter <์ด๋ฆ>` | `nest g f <์ด๋ฆ>` | `xxx.filter.ts` ์์ฑ |
| **๋ฏธ๋ค์จ์ด ์์ฑ** | `nest generate middleware <์ด๋ฆ>` | `nest g mi <์ด๋ฆ>` | `xxx.middleware.ts` ์์ฑ |
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์