Guider/Backend/BackendDevGuide0005
Backend#05

BackendDevGuide0005

TypeScript와 NestJS 완전 정복

🦅

BackendDevGuide0005

TypeScript와 NestJS 완전 정복 A-Z

비전공자도 OK! 현업에서 가장 많이 쓰는 TypeScript + NestJS를 처음부터 끝까지

⏱ 예상 학습: 3~4주 🏢 대기업 현업 표준 스택 💼 취업 필수 스킬

💡 왜 TypeScript + NestJS인가요?

98%
대형 백엔드 프로젝트 TypeScript 사용
#1
Node.js 프레임워크 만족도 순위
5x
TypeScript 사용 시 버그 감소율
Angular
영감을 받은 구조 → 대규모 팀 협업 최적

📚 학습 목차 — 13가지 핵심 주제

01 TypeScript란? JS와 차이점 완전 이해
02 TypeScript 기초 타입 시스템 완전 정복
03 인터페이스 vs 타입 별칭 + 제네릭
04 TypeScript 고급 타입 + 데코레이터
05 NestJS란? 구조와 핵심 개념
06 Module / Controller / Service / Provider
07 DTO + Validation + Pipe 완전 정복
08 Guard + Interceptor + Filter + Middleware
09 TypeORM / Prisma + NestJS 통합
10 JWT 인증 (Passport.js) + 권한 관리
11 Swagger API 문서 자동화
12 테스트 (Jest + E2E 테스트)
13 면접 Q&A + 실무 로드맵

📘 01. TypeScript란? — JS와 무엇이 다른가요?

TypeScript = JavaScript + 타입(Type) 정보. 레고로 비유하면 JS가 "아무 조각이나 끼울 수 있는 레고"라면, TypeScript는 "맞는 모양의 조각만 끼울 수 있는 레고"입니다!

❌ JavaScript (런타임에 오류 발견)

function add(a, b) {
  return a + b;
}
add(1, "2"); // "12" 반환 (버그!)
// 런타임에 실행돼야만 발견됨

✅ TypeScript (컴파일 시 즉시 오류 발견)

function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // ❌ 컴파일 에러!
// Argument of type 'string' is not assignable to
// parameter of type 'number'.
비교 항목 JavaScript TypeScript
타입 선언 ❌ 없음 (동적 타입) ✅ 정적 타입
오류 발견 시점 런타임 (실행 중) 컴파일 타임 (코딩 중)
자동완성 (IDE) ⚠️ 제한적 ✅ 완벽한 IntelliSense
리팩토링 ❌ 위험함 ✅ 안전한 대규모 리팩토링
학습 난이도 ✅ 낮음 ⚠️ 약간 높음 (그만큼 가치 있음)
대규모 팀 협업 ❌ 어려움 ✅ 최적

💻 TypeScript 설치 및 기본 설정

# TypeScript 전역 설치
npm install -g typescript

# 프로젝트 초기화
mkdir ts-project && cd ts-project
npm init -y
npm install typescript ts-node @types/node --save-dev

# tsconfig.json 생성
npx tsc --init

# tsconfig.json 핵심 설정
{
  "compilerOptions": {
    "target": "ES2020",          // 변환할 JS 버전
    "module": "commonjs",        // 모듈 방식
    "lib": ["ES2020"],
    "outDir": "./dist",          // 컴파일 결과 폴더
    "rootDir": "./src",          // 소스 폴더
    "strict": true,              // ✅ 엄격 모드 (강력 권장)
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,  // NestJS 필수
    "emitDecoratorMetadata": true    // NestJS 필수
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

# package.json scripts
{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts",
    "watch": "tsc --watch"
  }
}

🔤 02. TypeScript 기초 타입 시스템 완전 정복

TypeScript의 핵심은 타입입니다! 기본 타입부터 복잡한 객체 타입까지 모두 마스터합니다.

💻 기본 타입 완전 가이드

// ===== 1. 원시 타입 (Primitive Types) =====
const name: string = '홍길동';
const age: number = 25;
const isActive: boolean = true;
const nothing: null = null;
const notDefined: undefined = undefined;

// ===== 2. 배열 타입 =====
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ['홍길동', '김영희'];
const mixed: (string | number)[] = ['hello', 1]; // 유니온 타입 배열

// ===== 3. 객체 타입 =====
const user: { name: string; age: number; email?: string } = {
  name: '홍길동',
  age: 25
  // email은 ?가 있어서 생략 가능 (optional)
};

// ===== 4. 함수 타입 =====
// 매개변수 타입 + 반환 타입 명시
function greet(name: string, age: number): string {
  return `안녕하세요! 저는 ${name}이고 ${age}살입니다.`;
}

// 화살표 함수
const add = (a: number, b: number): number => a + b;

// void: 반환값 없는 함수
function logMessage(msg: string): void {
  console.log(msg);
}

// ===== 5. 유니온 타입 (Union Type) =====
type Status = 'active' | 'inactive' | 'pending';
let userStatus: Status = 'active';
userStatus = 'inactive'; // ✅ OK
userStatus = 'deleted';  // ❌ 컴파일 에러!

// ===== 6. 교차 타입 (Intersection Type) =====
type UserBase = { id: number; name: string };
type UserWithEmail = UserBase & { email: string };
// 두 타입을 모두 가짐

// ===== 7. Enum (열거형) =====
enum Role {
  USER = 'USER',
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER'
}

const userRole: Role = Role.ADMIN;
console.log(userRole); // "ADMIN"

// ===== 8. Tuple (튜플) — 고정된 길이와 타입의 배열 =====
const point: [number, number] = [10, 20]; // [x, y]
const userInfo: [string, number, boolean] = ['홍길동', 25, true];

// ===== 9. any vs unknown vs never =====
let anyVar: any = 'hello';   // 타입 검사 건너뜀 (사용 자제)
anyVar = 123;                  // OK - any는 모든 타입 허용

let unknown: unknown = 'hello'; // any보다 안전 - 사용 전 타입 확인 필요
if (typeof unknown === 'string') {
  console.log(unknown.toUpperCase()); // 타입 확인 후 사용 가능
}

// never: 절대 반환되지 않는 함수 (에러 throw, 무한루프)
function throwError(msg: string): never {
  throw new Error(msg);
}

🔧 03. 인터페이스 vs 타입 별칭 + 제네릭

TypeScript의 핵심 구조화 도구! 실무에서 매일 쓰는 패턴들입니다.

비교 interface type (Type Alias)
선언 병합 (재선언) ✅ 가능 (확장됨) ❌ 불가능
유니온/교차 타입 ❌ 불가능 ✅ 가능
클래스 implements ✅ 가능 ⚠️ 제한적
권장 사용처 객체 구조 정의, 클래스 계약 유니온, 복잡한 타입 조합

💻 Interface + Type + Generic 실전 패턴

// ===== Interface 사용 (객체 구조 정의) =====
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;           // optional
  readonly createdAt: Date; // 수정 불가
}

// Interface 확장 (extends)
interface Admin extends User {
  permissions: string[];
  department: string;
}

// ===== Type Alias 사용 =====
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiResponse<T> = {  // <T>가 제네릭!
  data: T;
  message: string;
  success: boolean;
  total?: number;
};

// ===== 제네릭 (Generic) — TypeScript 최대 강점 =====
// T는 사용할 때 결정되는 타입 (변수처럼 동작하는 타입)

// 제네릭 함수 — 어떤 타입이든 동작
function getFirst<T>(arr: T[]): T {
  return arr[0];
}
const firstNum = getFirst([1, 2, 3]);     // T = number
const firstStr = getFirst(['a', 'b']);    // T = string

// 실전 사용: API 응답 타입
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: '홍길동', email: 'hong@e.com', createdAt: new Date() },
  message: '성공',
  success: true
};
const usersResponse: ApiResponse<User[]> = {
  data: [...],
  message: '목록 조회 성공',
  success: true,
  total: 100
};

// 제네릭 클래스
class Repository<T> {
  private items: T[] = [];
  
  add(item: T): void { this.items.push(item); }
  getAll(): T[] { return this.items; }
  findById(id: number): T | undefined {
    return (this.items as any[]).find(item => item.id === id);
  }
}

const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: '홍길동', email: 'hong@e.com', createdAt: new Date() });

⚡ 04. TypeScript 고급 타입 + 데코레이터

실무에서 자주 쓰는 Utility Types와 NestJS의 핵심 기능인 데코레이터를 배웁니다.

💻 Utility Types — 타입 변환 도구

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  age: number;
}

// Partial<T>: 모든 필드를 optional로 (업데이트 DTO에 유용)
type UpdateUserDto = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required<T>: 모든 필드를 required로
type RequiredUser = Required<User>;

// Pick<T, K>: 일부 필드만 선택
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string } — password 제외!

// Omit<T, K>: 일부 필드 제외
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; age: number }

// Record<K, V>: 키-값 타입의 객체
type UserMap = Record<string, User>;
const userMap: UserMap = { 'user_1': {...}, 'user_2': {...} };

// Readonly<T>: 모든 필드를 수정 불가로
type FrozenUser = Readonly<User>;

// ReturnType<T>: 함수의 반환 타입 추출
function getUser() { return { id: 1, name: '홍길동' }; }
type GetUserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }

// Awaited<T>: Promise 결과 타입 추출
type ResolvedUser = Awaited<Promise<User>>; // User

💻 데코레이터(Decorator) — NestJS의 심장

데코레이터는 클래스나 메서드에 @기호로 기능을 추가하는 특수 문법입니다. 음식점 비유: 요리사(클래스)에게 배지(데코레이터)를 붙여 역할을 표시하는 것!

// ===== 클래스 데코레이터 =====
function Controller(path: string) {
  return function(constructor: Function) {
    Reflect.defineMetadata('path', path, constructor);
    console.log(`Controller registered: ${path}`);
  };
}

// ===== 메서드 데코레이터 =====
function Get(path: string) {
  return function(target: any, propertyKey: string) {
    Reflect.defineMetadata('method', 'GET', target, propertyKey);
    Reflect.defineMetadata('path', path, target, propertyKey);
  };
}

// ===== 실전 사용 (NestJS 스타일) =====
@Controller('/users')           // 라우트 prefix: /users
class UsersController {
  
  @Get('/')                     // GET /users
  findAll() { ... }
  
  @Get('/:id')                 // GET /users/:id
  findOne(@Param('id') id: string) { ... }
}

// ===== 프로퍼티 데코레이터 (TypeORM/Prisma에서 자주 사용) =====
class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column({ unique: true })
  email: string;
  
  @Column()
  name: string;
}

🏗️ 05. NestJS란? — 구조와 핵심 개념

NestJS = Node.js + TypeScript + Angular스러운 아키텍처. 대규모 서비스에 최적화된 프레임워크입니다!

🍽️ 음식점 비유로 NestJS 이해하기

🏪

Module

음식점 지점
관련 기능을 묶는 단위

🧑‍💼

Controller

홀 직원 (웨이터)
주문 받고 주방에 전달

👨‍🍳

Service

주방 (요리사)
실제 비즈니스 로직 처리

🗄️

Repository

냉장고 (창고)
데이터 저장/조회

💻 NestJS 설치 및 프로젝트 구조

# NestJS CLI 설치
npm install -g @nestjs/cli

# 프로젝트 생성
nest new my-nestjs-app
cd my-nestjs-app
npm run start:dev

# 프로젝트 구조
my-nestjs-app/
├── src/
│   ├── app.module.ts       # 루트 모듈 (모든 것의 시작)
│   ├── app.controller.ts   # 기본 컨트롤러
│   ├── app.service.ts      # 기본 서비스
│   └── main.ts             # 진입점 (포트 설정)
├── test/
├── nest-cli.json
├── tsconfig.json
└── package.json

# 리소스 자동 생성 (CRUD 포함)
nest generate resource users     # users 모듈 전체 생성
nest generate module users       # 모듈만
nest generate controller users   # 컨트롤러만
nest generate service users      # 서비스만

💻 main.ts — NestJS 앱 부트스트랩

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // ① 전역 Validation Pipe 설정 (DTO 자동 검증)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,       // DTO에 없는 속성 자동 제거
    forbidNonWhitelisted: true,  // 허용되지 않은 속성 에러
    transform: true,      // 타입 자동 변환 (string → number)
  }));
  
  // ② API 접두사 설정
  app.setGlobalPrefix('api/v1');
  
  // ③ CORS 설정
  app.enableCors({
    origin: process.env.FRONTEND_URL || 'http://localhost:3001',
    credentials: true,
  });
  
  // ④ Swagger 설정 (API 문서 자동 생성)
  const config = new DocumentBuilder()
    .setTitle('My API')
    .setDescription('My API Documentation')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api/docs', app, document);
  
  await app.listen(3000);
  console.log(`🚀 서버 실행 중: http://localhost:3000`);
  console.log(`📚 API 문서: http://localhost:3000/api/docs`);
}
bootstrap();

🧩 06. Module / Controller / Service / Provider

NestJS의 핵심 4요소! 실전 User CRUD API를 만들면서 완전히 익힙니다.

💻 users.module.ts — 모듈 (모든 것을 조립)

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
  imports: [PrismaModule],       // 이 모듈에서 사용할 다른 모듈
  controllers: [UsersController], // 이 모듈의 컨트롤러 등록
  providers: [UsersService],      // 이 모듈의 서비스/프로바이더 등록
  exports: [UsersService],        // 다른 모듈에서 쓸 수 있도록 내보내기
})
export class UsersModule {}

💻 users.controller.ts — 컨트롤러 (HTTP 요청 처리)

import { Controller, Get, Post, Body, Param, Put, Delete, 
         UseGuards, ParseIntPipe, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')  // 라우트 prefix: /api/v1/users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
  // ↑ NestJS DI (의존성 주입): 자동으로 UsersService 인스턴스 주입

  @Get()                         // GET /api/v1/users
  findAll(
    @Query('page') page: string = '1',
    @Query('limit') limit: string = '10'
  ) {
    return this.usersService.findAll(+page, +limit);
  }

  @Get(':id')                   // GET /api/v1/users/:id
  findOne(@Param('id', ParseIntPipe) id: number) {
    // ParseIntPipe: string → number 자동 변환
    return this.usersService.findOne(id);
  }

  @Post()                        // POST /api/v1/users
  create(@Body() createUserDto: CreateUserDto) {
    // @Body()에 DTO 지정 → 자동으로 검증됨!
    return this.usersService.create(createUserDto);
  }

  @Put(':id')                   // PUT /api/v1/users/:id
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')                // DELETE /api/v1/users/:id
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}

💻 users.service.ts — 서비스 (비즈니스 로직)

import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcryptjs';

@Injectable()  // 이 클래스를 DI 컨테이너에 등록
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async findAll(page: number, limit: number) {
    const skip = (page - 1) * limit;
    const [users, total] = await Promise.all([
      this.prisma.user.findMany({
        skip,
        take: limit,
        select: { id: true, name: true, email: true, createdAt: true },
        // password는 절대 반환하지 않음!
      }),
      this.prisma.user.count()
    ]);
    return { data: users, total, page, totalPages: Math.ceil(total / limit) };
  }

  async findOne(id: number) {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: { id: true, name: true, email: true, createdAt: true }
    });
    if (!user) throw new NotFoundException(`사용자 ${id}를 찾을 수 없습니다.`);
    // NestJS 표준 예외: 자동으로 404 응답 생성
    return user;
  }

  async create(dto: CreateUserDto) {
    const existing = await this.prisma.user.findUnique({ where: { email: dto.email } });
    if (existing) throw new ConflictException('이미 사용 중인 이메일입니다.');
    
    const hashedPw = await bcrypt.hash(dto.password, 12);
    return this.prisma.user.create({
      data: { ...dto, password: hashedPw },
      select: { id: true, name: true, email: true }
    });
  }
}

📋 07. DTO + Validation + Pipe 완전 정복

DTO(Data Transfer Object)는 API로 들어오고 나가는 데이터의 형태를 정의합니다. class-validator로 자동 검증까지!

💻 DTO 완전 가이드

// npm install class-validator class-transformer
import { IsString, IsEmail, IsNumber, Min, Max, IsOptional, 
         Length, Matches, IsEnum } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

// ===== create-user.dto.ts =====
export class CreateUserDto {
  @ApiProperty({ example: '홍길동', description: '사용자 이름' })
  @IsString()
  @Length(2, 50, { message: '이름은 2~50자이어야 합니다.' })
  name: string;

  @ApiProperty({ example: 'hong@example.com' })
  @IsEmail({}, { message: '올바른 이메일 형식이 아닙니다.' })
  email: string;

  @ApiProperty({ example: 'Password123!', minLength: 8 })
  @IsString()
  @Length(8, 128)
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])/, {
    message: '비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'
  })
  password: string;

  @ApiPropertyOptional({ example: 25 })
  @IsOptional()
  @IsNumber()
  @Min(0) @Max(150)
  age?: number;
}

// ===== update-user.dto.ts — Partial로 모두 선택 사항 =====
import { PartialType } from '@nestjs/swagger'; // 또는 @nestjs/mapped-types
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// CreateUserDto의 모든 필드가 optional이 됨 — 코드 재사용!

// ===== 응답 DTO (보안: 비밀번호 제외) =====
export class UserResponseDto {
  @ApiProperty() id: number;
  @ApiProperty() name: string;
  @ApiProperty() email: string;
  @ApiProperty() createdAt: Date;
  // password 필드 없음 → 절대 응답에 포함 안 됨
}

// ===== 검증 결과 예시 =====
// 잘못된 요청: { "name": "홍", "email": "invalid-email", "password": "1234" }
// 자동 응답:
{
  "statusCode": 400,
  "message": [
    "이름은 2~50자이어야 합니다.",
    "올바른 이메일 형식이 아닙니다.",
    "비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다."
  ],
  "error": "Bad Request"
}

💻 커스텀 Pipe 만들기

// NestJS 기본 내장 Pipe
// ParseIntPipe: string → number
// ParseBoolPipe: string → boolean
// ParseUUIDPipe: UUID 형식 검증
// ValidationPipe: DTO 검증
// DefaultValuePipe: 기본값 설정

@Get()
findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number
) {
  return this.usersService.findAll(page, limit);
}

// 커스텀 Pipe — 양 끝 공백 제거
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any) {
    if (typeof value === 'string') return value.trim();
    if (typeof value === 'object' && value !== null) {
      Object.keys(value).forEach(key => {
        if (typeof value[key] === 'string') value[key] = value[key].trim();
      });
    }
    return value;
  }
}

// 사용
@Post()
create(@Body(new TrimPipe()) dto: CreateUserDto) { ... }

🛡️ 08. Guard + Interceptor + Filter + Middleware

요청이 컨트롤러에 도달하기 전/후를 가로채는 NestJS의 고급 기능들! 미들웨어 파이프라인을 완전 이해합니다.

🔄 요청 처리 순서

📨 HTTP 요청
Middleware
Guard
Interceptor
(전처리)
Pipe
Controller
응답 반환
Interceptor
(후처리)
📤 HTTP 응답
  (오류 시)
Exception
Filter

💻 Guard — 인증/인가 처리

// JWT 인증 가드
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } 
  from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new UnauthorizedException('토큰이 없습니다.');
    
    try {
      const payload = await this.jwtService.verifyAsync(token);
      request.user = payload; // req.user에 페이로드 저장
    } catch {
      throw new UnauthorizedException('유효하지 않은 토큰입니다.');
    }
    return true;
  }

  private extractTokenFromHeader(request: any): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

// 역할(Role) 가드
import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  
  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) return true; // 롤 설정 없으면 통과
    const { user } = context.switchToHttp().getRequest();
    return roles.includes(user.role);
  }
}

// 사용 예시
@Get('/admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN')
adminOnly() { return '관리자만 접근 가능'; }

💻 Interceptor + Exception Filter

// ① 응답 형식 통일 Interceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 
  from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString()
      }))
    );
  }
}

// ② 전역 Exception Filter — 에러 응답 통일
import { ExceptionFilter, Catch, HttpException, HttpStatus } 
  from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: any) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;
    
    response.status(status).json({
      success: false,
      statusCode: status,
      message: exception instanceof HttpException 
        ? exception.message : 'Internal Server Error',
      timestamp: new Date().toISOString()
    });
  }
}

// main.ts에서 전역 등록
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new AllExceptionsFilter());

🗄️ 09. TypeORM / Prisma + NestJS 통합

NestJS에서 DB를 연결하는 두 가지 방법! Prisma(현업 추세)와 TypeORM(전통 방식) 모두 배웁니다.

비교 Prisma TypeORM
타입 안전성 ✅ 자동 생성 (완벽) ⚠️ 수동 작업 필요
스키마 정의 위치 schema.prisma 파일 Entity 클래스 (데코레이터)
마이그레이션 ✅ 자동/직관적 ⚠️ 복잡할 수 있음
2025년 트렌드 ✅ 빠르게 성장 중 ⚠️ 레거시 코드베이스 많음
권장 ✅ 신규 프로젝트 기존 프로젝트 유지보수

💻 Prisma + NestJS 통합 완전 가이드

# Prisma 설치
npm install prisma @prisma/client
npx prisma init

# prisma/schema.prisma — 데이터 모델
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  password  String
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String   @db.Text
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}
enum Role { USER ADMIN MANAGER }

# 마이그레이션 실행
npx prisma migrate dev --name init

// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient 
  implements OnModuleInit, OnModuleDestroy {
  
  async onModuleInit() {
    await this.$connect();
    console.log('✅ DB 연결 성공');
  }
  
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

// prisma/prisma.module.ts
@Module({
  providers: [PrismaService],
  exports: [PrismaService],  // 다른 모듈에서 사용 가능하도록
})
export class PrismaModule {}

🔐 10. JWT 인증 (Passport.js) + 권한 관리

NestJS에서 Passport.js와 JWT를 통합하는 표준 방법입니다!

💻 JWT Auth 모듈 완전 구현

# 설치
npm install @nestjs/passport passport @nestjs/jwt passport-jwt
npm install -D @types/passport-jwt

// auth/auth.module.ts
@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [JwtModule],
})
export class AuthModule {}

// auth/strategies/jwt.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.get('JWT_SECRET'),
    });
  }
  async validate(payload: { userId: number; role: string }) {
    return payload; // req.user에 저장됨
  }
}

// auth/auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
  ) {}

  async login(dto: LoginDto) {
    const user = await this.prisma.user.findUnique({ where: { email: dto.email } });
    if (!user || !(await bcrypt.compare(dto.password, user.password))) {
      throw new UnauthorizedException('이메일 또는 비밀번호가 올바르지 않습니다.');
    }
    const payload = { userId: user.id, role: user.role };
    return {
      accessToken: await this.jwtService.signAsync(payload),
    };
  }
}

// 사용 — 데코레이터로 깔끔하게!
@Get('profile')
@UseGuards(AuthGuard('jwt'))
getProfile(@Request() req) {
  return req.user; // { userId, role, iat, exp }
}

📚 11. Swagger API 문서 자동화

코드를 쓰면 API 문서가 자동으로 생성됩니다! 프론트엔드 개발자와 협업할 때 필수입니다.

💻 Swagger 완전 설정

# 설치
npm install @nestjs/swagger swagger-ui-express

// DTO에 Swagger 데코레이터 추가
import { ApiProperty, ApiPropertyOptional, ApiHideProperty } 
  from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ 
    example: '홍길동', 
    description: '사용자 이름 (2-50자)',
    minLength: 2,
    maxLength: 50
  })
  @IsString()
  name: string;

  @ApiProperty({ example: 'hong@example.com', format: 'email' })
  @IsEmail()
  email: string;

  @ApiProperty({ example: 'Password123!', format: 'password' })
  @IsString()
  password: string;

  @ApiPropertyOptional({ example: 25 })
  @IsOptional()
  @IsNumber()
  age?: number;
}

// Controller에 Swagger 데코레이터
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } 
  from '@nestjs/swagger';

@ApiTags('users')      // Swagger에서 그룹화
@Controller('users')
export class UsersController {
  
  @Get()
  @ApiOperation({ summary: '사용자 목록 조회', description: '페이지네이션으로 조회' })
  @ApiResponse({ status: 200, description: '조회 성공', type: [UserResponseDto] })
  findAll() { ... }

  @Post()
  @ApiOperation({ summary: '사용자 생성' })
  @ApiResponse({ status: 201, description: '생성 성공', type: UserResponseDto })
  @ApiResponse({ status: 400, description: '입력값 오류' })
  @ApiResponse({ status: 409, description: '이메일 중복' })
  create(@Body() dto: CreateUserDto) { ... }

  @Get('profile')
  @ApiBearerAuth()            // JWT 필요 표시
  @UseGuards(JwtAuthGuard)
  getProfile() { ... }
}
// 결과: http://localhost:3000/api/docs 에서 API 문서 확인!

🧪 12. 테스트 (Jest + E2E 테스트)

현업에서 테스트는 선택이 아닌 필수! NestJS는 Jest가 기본 내장되어 있습니다.

테스트 종류 설명 파일명 NestJS 명령
Unit Test 서비스/클래스 단독 테스트 *.spec.ts npm test
E2E Test 실제 HTTP 요청으로 전체 흐름 테스트 *.e2e-spec.ts npm run test:e2e

💻 Unit Test + E2E Test 예시

// users.service.spec.ts — Unit Test
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';

describe('UsersService', () => {
  let service: UsersService;
  let prisma: PrismaService;

  const mockPrisma = {
    user: {
      findMany: jest.fn(),
      findUnique: jest.fn(),
      create: jest.fn(),
      count: jest.fn(),
    }
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: PrismaService, useValue: mockPrisma }
      ],
    }).compile();
    service = module.get<UsersService>(UsersService);
  });

  it('사용자를 생성할 수 있다', async () => {
    const dto = { name: '홍길동', email: 'hong@e.com', password: 'Test123!' };
    mockPrisma.user.findUnique.mockResolvedValue(null);
    mockPrisma.user.create.mockResolvedValue({ id: 1, ...dto });
    
    const result = await service.create(dto);
    expect(result.email).toBe('hong@e.com');
  });

  it('존재하지 않는 사용자 조회 시 NotFoundException을 던진다', async () => {
    mockPrisma.user.findUnique.mockResolvedValue(null);
    await expect(service.findOne(999)).rejects.toThrow('찾을 수 없습니다');
  });
});

// app.e2e-spec.ts — E2E Test
import * as request from 'supertest';

describe('Users (e2e)', () => {
  it('POST /api/v1/users — 회원가입 성공', () => {
    return request(app.getHttpServer())
      .post('/api/v1/users')
      .send({ name: '테스터', email: 'test@e.com', password: 'Test123!' })
      .expect(201)
      .expect(res => expect(res.body.data.email).toBe('test@e.com'));
  });
});

🎤 13. 면접 Q&A + 실무 체크리스트 + 로드맵

TypeScript + NestJS 면접에서 꼭 나오는 질문들과 현업 실전 체크리스트!

Q1. TypeScript를 왜 사용하나요? JavaScript와의 차이점은?

TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어입니다. 주요 이점은 ① 코딩 중 실시간 오류 발견 (런타임이 아닌 컴파일 타임), ② IDE 자동완성으로 생산성 향상, ③ 리팩토링 안전성 (타입이 맞지 않으면 에러), ④ 팀 협업 시 코드 의도를 명확히 전달할 수 있습니다. 대규모 프로젝트일수록 효과가 큽니다.

Q2. NestJS의 의존성 주입(DI)이란 무엇인가요?

의존성 주입은 객체가 필요로 하는 의존성을 외부(NestJS IoC 컨테이너)에서 주입받는 패턴입니다. 예를 들어 UsersController는 UsersService를 직접 생성하지 않고, NestJS가 자동으로 생성해서 생성자에 주입합니다. 장점: ① 결합도 감소, ② 테스트 시 Mock 객체 주입 용이, ③ 코드 재사용성 향상입니다.

Q3. interface와 type의 차이는?

가장 큰 차이는 선언 병합(Declaration Merging)입니다. interface는 같은 이름으로 여러 번 선언하면 합쳐지지만, type은 중복 선언이 불가능합니다. 또한 type은 유니온(|), 교차(&) 등 복잡한 타입 조합이 가능합니다. 현업에서는 객체 구조 정의엔 interface, 복잡한 타입 조합엔 type을 주로 사용합니다.

Q4. NestJS의 Guard, Interceptor, Filter, Middleware 차이는?

Middleware: Express와 동일, 요청 전처리 (로깅, CORS). Guard: 인증/인가 확인 (true/false 반환). Interceptor: 요청/응답 변환 (응답 형식 통일, 캐싱). Filter: 예외 처리 (에러 응답 포맷 통일). 실행 순서: Middleware → Guard → Interceptor(전) → Pipe → Controller → Interceptor(후) → Filter(오류 시)

Q5. 제네릭(Generic)이란? 왜 사용하나요?

제네릭은 타입을 매개변수처럼 사용하는 기능입니다. 예를 들어 ApiResponse<T>는 T에 User를 넣으면 ApiResponse<User>, Product를 넣으면 ApiResponse<Product>가 됩니다. 코드 재사용성을 높이면서도 타입 안전성을 유지할 수 있습니다. any 타입의 타입 안전한 대안입니다.

Q6. NestJS 모듈 시스템이란? 왜 중요한가요?

NestJS 모듈은 기능 단위로 코드를 캡슐화하는 구조입니다. 각 모듈은 자신만의 Controller, Service, Provider를 가지며, exports로 다른 모듈과 공유합니다. 이 방식은 ① 코드 응집도 향상, ② 관심사 분리, ③ 독립적 테스트 가능, ④ 마이크로서비스 전환 용이라는 장점을 줍니다.

📋 TypeScript + NestJS 현업 체크리스트

✅ TypeScript 기초

  • strict 모드 활성화
  • any 사용 최소화 → unknown 활용
  • Utility Types 활용 (Pick, Omit, Partial)
  • enum 또는 as const 활용
  • 타입 가드 작성
  • 제네릭으로 코드 재사용

🚀 NestJS 필수 설정

  • ValidationPipe 전역 설정
  • 글로벌 에러 Filter 설정
  • 응답 Interceptor 설정
  • ConfigModule 환경변수 관리
  • Swagger 문서 자동화
  • CORS + Helmet 설정

🏆 현업 베스트 프랙티스

  • DTO에서 password 응답 제외
  • Unit Test 커버리지 80%+ 목표
  • 모듈별 독립 테스트
  • 환경별 .env 분리 (dev/prod)
  • Prisma 마이그레이션 관리
  • E2E 테스트로 API 검증

🗺️ TypeScript + NestJS 학습 로드맵

단계 학습 내용 기간 결과물
1단계 TypeScript 기초 (타입, 인터페이스, 제네릭) 1주 타입이 있는 JS 코드
2단계 NestJS 기초 (Module/Controller/Service) 1주 기본 CRUD API
3단계 DTO + Validation + Prisma 통합 1주 DB 연동 REST API
4단계 JWT 인증 + Guard + Interceptor 1주 인증 포함 API
5단계 Swagger 문서화 + Unit/E2E 테스트 1주 완성된 백엔드 API
6단계 배포 (Docker + CI/CD) 1주 프로덕션 배포 완료

🎉 BackendDevGuide0005 완료!

TypeScript와 NestJS의 모든 핵심을 마스터했습니다. 이제 타입 안전한 코드로 대규모 백엔드 API를 구축할 수 있습니다!

다음: BackendDevGuide0006 — 테스트 심화 + TDD 완전 정복

반응형