[NestJS] NestJs 소개와 기본 구조

NestJS #1 — 소개와 기본 구조 | issac's TIL

NestJS는 Node.js 위에서 작동하는 서버 프레임워크다. 기본적으로 Express를 내부 HTTP 플랫폼으로 사용하고, 필요에 따라 Fastify로 교체할 수도 있다.

Express가 자유도가 높지만 대규모 프로젝트에서 구조가 파편화되기 쉬운 것과 달리, NestJS는 Angular에서 영감을 받은 모듈 기반 아키텍처를 강제한다. 덕분에 팀 단위의 협업이나 대규모 애플리케이션에서 일관된 구조를 유지하기가 쉽다.

NestJS의 핵심 특징
TypeScript를 기본 언어로 사용하고, OOP(객체지향), FP(함수형), RP(반응형) 프로그래밍 패러다임을 모두 지원한다. 데코레이터 기반의 선언적 코드 스타일로, 라우터나 미들웨어를 설정하는 방식이 Express와 상당히 다르다.
언어
TypeScript 우선

JavaScript도 지원하지만, 타입 안전성과 데코레이터를 제대로 활용하려면 TypeScript가 사실상 필수다.

구조
모듈 기반

기능별로 Module을 만들고, 그 안에서 Controller와 Service가 역할을 분리한다. Angular 아키텍처에서 직접 영향을 받았다.

HTTP
Express/Fastify

내부적으로 Express를 래핑해서 사용한다. 기존 Express 미들웨어를 그대로 붙일 수 있다.

Express
// 구조는 직접 설계해야 한다
const express = require('express');
const app = express();

app.get('/boards', (req, res) => {
  res.json({ boards: [] });
});

app.listen(3000);
NestJS
// 구조가 프레임워크에 의해 강제된다
@Controller('boards')
export class BoardsController {
  constructor(
    private boardsService: BoardsService
  ) {}

  @Get()
  getAllBoards() {
    return this.boardsService.getAllBoards();
  }
}

환경 설정

Node.js가 설치되어 있다면 NestJS CLI를 글로벌로 설치하는 것만으로 개발 환경이 갖춰진다.

# Node.js 버전 확인 (14 이상 필요) $ node -v

# NestJS CLI 글로벌 설치 $ npm i -g @nestjs/cli

# 설치 확인 $ nest --version
# 프로젝트 생성 (패키지 매니저 선택 창이 나옴) $ nest new nestjs-board-app

# 개발 서버 시작 (파일 변경 감지, 자동 재시작) $ npm run start:dev
npm vs yarn
CLI가 패키지 매니저를 물어볼 때 npm을 선택하면 된다. 프로젝트 생성 후 http://localhost:3000에 접속하면 Hello World!가 뜨는 것으로 정상 작동을 확인할 수 있다.

게시물 CRUD 앱 소개

이 강의에서 만드는 것은 게시물(Board) CRUD 애플리케이션이다. 단계별로 기능을 쌓아가는 방식으로 진행한다.

메모리 기반 CRUD

배열에 게시물을 저장하고 조회·생성·삭제·업데이트를 구현한다. 서버를 재시작하면 데이터가 사라진다.

Pipes와 유효성 검증

class-validator로 요청 바디를 검증하고, 커스텀 파이프로 enum 상태값을 필터링한다.

PostgreSQL + TypeORM

메모리 배열을 실제 데이터베이스로 교체한다. TypeORM Repository 패턴으로 DB 작업을 추상화한다.


기본 프로젝트 구조

nest new로 프로젝트를 생성하면 아래와 같은 구조가 만들어진다. 처음에는 파일이 많아 보이지만, 핵심은 src/ 폴더 안의 파일들이다.

nestjs-board-app/
├── nest-cli.json Nest CLI 설정 (빌드 옵션)
├── tsconfig.json TypeScript 컴파일러 설정
├── tsconfig.build.json 빌드 전용 TS 설정
├── package.json 의존성, 스크립트
└── src/
    ├── main.ts 앱 진입점 — NestFactory로 앱 인스턴스 생성
    ├── app.module.ts 루트 모듈 — 모든 모듈의 시작점
    ├── app.controller.ts 루트 컨트롤러
    ├── app.controller.spec.ts 컨트롤러 단위 테스트
    └── app.service.ts 루트 서비스

main.ts

애플리케이션의 시작점이다. NestFactory.create()로 앱 인스턴스를 만들고, 포트를 지정해 HTTP 서버를 시작한다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

app.module.ts

루트 모듈이다. NestJS 앱은 항상 하나의 루트 모듈을 시작점으로, 그 아래에 기능별 모듈을 트리 형태로 붙인다.

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()의 속성 네 가지
providers: 이 모듈에서 사용할 서비스(주입 가능한 클래스)를 등록한다.
controllers: 이 모듈에서 사용할 컨트롤러를 등록한다.
imports: 다른 모듈을 가져와서 그 모듈의 providers를 사용할 수 있게 한다.
exports: 이 모듈의 providers를 다른 모듈에서도 사용할 수 있도록 내보낸다.

NestJS 로직 흐름

HTTP 요청이 들어오면 NestJS 내부에서 다음 순서로 처리된다.

1
Client (브라우저 / Postman)

GET /boards 같은 HTTP 요청을 보낸다.

2
Controller

요청을 받아서 어떤 서비스 메서드를 호출할지 결정한다. 비즈니스 로직을 직접 담지 않는다.

3
Service

실제 비즈니스 로직이 여기에 있다. DB 조회, 데이터 가공 등을 수행하고 결과를 컨트롤러에 돌려준다.

4
Client

컨트롤러가 서비스의 반환값을 HTTP 응답으로 직렬화해서 클라이언트에 보낸다.

왜 Controller와 Service를 나누는가
컨트롤러는 "어떤 요청을 받아서 어디로 보낼지"만 담당하고, 서비스는 "어떻게 처리할지"만 담당한다. 이렇게 분리하면 서비스 로직을 여러 컨트롤러에서 재사용할 수 있고, 테스트 코드 작성도 쉬워진다.

NestJS 모듈

모듈은 NestJS 앱을 구성하는 기본 단위다. @Module() 데코레이터를 붙인 클래스가 모듈이다. 앱은 적어도 하나의 루트 모듈에서 시작하고, 기능이 늘어날수록 기능별로 모듈을 추가한다.

모듈은 기본적으로 싱글톤이다. 하나의 모듈 인스턴스를 여러 모듈이 공유할 수 있다.

providers

이 모듈 안에서 주입 가능한 서비스·리포지토리·팩토리 등을 등록한다

controllers

이 모듈이 소유하는 컨트롤러 클래스들을 등록한다

imports

필요한 외부 모듈을 가져온다. 해당 모듈이 exports한 것을 사용 가능해진다

exports

이 모듈의 providers 중 다른 모듈에서도 쓸 수 있도록 공개할 항목을 선언한다

Board 모듈 생성

CLI로 Board 기능을 담당하는 모듈을 생성한다. 이 명령을 실행하면 src/boards/boards.module.ts 파일이 자동으로 만들어지고, app.module.tsimports에도 자동 등록된다.

$ nest g module boards
// src/boards/boards.module.ts
import { Module } from '@nestjs/common';

@Module({})
export class BoardsModule {}
// src/app.module.ts (자동 업데이트됨)
import { Module } from '@nestjs/common';
import { BoardsModule } from './boards/boards.module';

@Module({
  imports: [BoardsModule],
})
export class AppModule {}

NestJS Controller

컨트롤러는 클라이언트의 HTTP 요청을 받고 응답을 반환하는 역할을 한다. 클라이언트에서 어떤 요청이 어떤 컨트롤러 메서드에 연결되는지는 라우팅으로 결정된다.

@Controller('boards')처럼 데코레이터에 경로를 지정하면, 해당 컨트롤러의 모든 라우트가 그 경로를 기준으로 동작한다. 메서드에는 @Get(), @Post(), @Delete(), @Patch() 같은 HTTP 메서드 데코레이터를 붙인다.

핸들러(Handler)란
@Get(), @Post() 등 HTTP 메서드 데코레이터가 붙은 컨트롤러 내부의 메서드를 핸들러라고 부른다. 핸들러가 요청을 처리하고 응답 데이터를 반환한다.

Board Controller 생성

--no-spec 옵션은 테스트 파일(.spec.ts)을 함께 생성하지 않는다는 의미다. 이 명령을 실행하면 boards.module.tscontrollers에도 자동 등록된다.

$ nest g controller boards --no-spec
// src/boards/boards.controller.ts
import { Controller } from '@nestjs/common';

@Controller('boards')
export class BoardsController {}
// src/boards/boards.module.ts (자동 업데이트됨)
import { Module } from '@nestjs/common';
import { BoardsController } from './boards.controller';

@Module({
  controllers: [BoardsController],
})
export class BoardsModule {}

NestJS Providers와 Service

NestJS에서 Provider는 의존성으로 주입될 수 있는 모든 것을 가리킨다. 서비스, 리포지토리, 팩토리, 헬퍼 등이 모두 Provider가 될 수 있다. Provider로 등록하려면 클래스에 @Injectable() 데코레이터를 붙이고, 모듈의 providers에 등록해야 한다.

Service는 Provider의 가장 대표적인 형태다. 비즈니스 로직을 담고, 컨트롤러에서 생성자를 통해 주입받아 사용한다. NestJS가 주입 과정을 관리하기 때문에, 컨트롤러 클래스 안에서 직접 인스턴스를 생성할 필요가 없다.

의존성 주입(Dependency Injection)이란
클래스 내부에서 new SomeService()처럼 직접 인스턴스를 만드는 대신, 외부(NestJS IoC 컨테이너)에서 인스턴스를 만들어 생성자로 전달해주는 패턴이다. 클래스 간의 결합도가 낮아지고, 테스트 시 의존성을 쉽게 교체할 수 있다.

Board Service 생성

CLI로 서비스를 생성하면 @Injectable()이 자동으로 붙고, boards.module.tsproviders에도 자동 등록된다.

$ nest g service boards --no-spec
// src/boards/boards.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class BoardsService {}

서비스를 컨트롤러에서 사용하려면, 컨트롤러 생성자에서 TypeScript의 접근 제어자(private)와 함께 주입받는다. private을 쓰면 생성자 파라미터가 자동으로 인스턴스 속성이 된다.

// src/boards/boards.controller.ts
import { Controller } from '@nestjs/common';
import { BoardsService } from './boards.service';

@Controller('boards')
export class BoardsController {
  constructor(private boardsService: BoardsService) {}
}
// src/boards/boards.module.ts (자동 업데이트됨)
import { Module } from '@nestjs/common';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';

@Module({
  controllers: [BoardsController],
  providers: [BoardsService],
})
export class BoardsModule {}
생성자 주입 패턴
constructor(private boardsService: BoardsService)에서 private은 TypeScript 단축 문법이다. 이 한 줄이 private boardsService: BoardsService 프로퍼티 선언 + 생성자 내 할당을 동시에 수행한다. NestJS가 DI 컨테이너를 통해 BoardsService 인스턴스를 자동으로 만들어서 주입한다.

'Study > Node.JS' 카테고리의 다른 글

[NestJS] PostgreSQL과 TypeORM  (0) 2026.05.24
[Nest.js] 메모리 CRUD와 Pipes  (0) 2026.05.17
[Express] 인증 & JWT & OAuth  (0) 2026.04.08
[Express] CORS & Swagger 세팅  (0) 2026.04.08
[Express] Express 미들웨어 & 에러 핸들링  (1) 2026.04.08