첫 번째 NestJS 애플리케이션 만들기
Nest CLI가 만든 핵심 파일의 책임을 구분하고 부트스트랩부터 컨트롤러 응답까지 첫 요청의 흐름을 추적합니다.
NestJS 개발 환경 구성이 완료되었습니다.
앞서 생성하고 실행해 본 Hello World! 애플리케이션은 NestJS의 가장 기본적인 형태를 보여줬습니다.
이번 절에서는 이 애플리케이션의 기본 구조를 자세히 살펴보고, Hello World! 메시지가 화면에 나타나는 과정, 즉 NestJS가 요청을 처리하는 흐름을 함께 파헤쳐 보겠습니다.
요청 전 · 부트스트랩
main.ts가 루트 모듈 전달NestFactory.create(AppModule)이 AppModule 클래스와 메타데이터를 부트스트랩 입력으로 사용한다.- AppModule이 조립 정보 등록
controllers와providers배열이 컨테이너가 관리할 대상을 알린다. - DI 컨테이너가 의존성 연결
기본 범위 provider를 생성하고 AppController 생성자에 AppService를 주입한다.
요청마다 · HTTP 처리
GET /요청브라우저가 애플리케이션의 설정된 port로 루트 요청을 보낸다.
- AppController route 일치
@Controller()와@Get()이 접두사 없는GET /handler를 선택한다. - 이미 주입된 AppService 호출
getHello()가'Hello World!'문자열을 반환한다. - 문자열 그대로 응답
표준 응답 처리에서 primitive string은 JSON 직렬화 없이 HTTP 응답으로 전송된다.
app.listen(3000)을 사용하고, 현재 First Steps 문서는 app.listen(process.env.PORT ?? 3000) 예를 제시한다. 어느 형태든 실제 listener가 받은 port가 요청 주소의 기준이다.프로젝트 기본 구조 이해하기
NestJS CLI를 통해 nest new 명령어로 프로젝트를 생성하면 다음과 같은 디렉토리 및 파일 구조를 갖게 됩니다.
모든 파일을 다 살펴보기보다는 지금 당장 우리가 이해해야 할 핵심적인 파일들을 중심으로 살펴보겠습니다.
| 핵심 단위 | 책임 | Hello World 기여 | 변경 경계 |
|---|---|---|---|
main.ts |
애플리케이션 진입점과 HTTP listener 시작 | NestFactory.create(AppModule)로 부트스트랩하고 설정된 port를 listen |
전역 pipe·prefix, HTTP adapter, port 설정이 바뀔 때 |
AppModule |
루트 모듈의 구성 메타데이터 | AppController와 AppService를 등록해 DI 컨테이너에 조립 정보를 제공 | module import나 controller·provider 등록 경계가 바뀔 때 |
AppController |
HTTP 경로와 입력·응답 계약 | 접두사 없는 GET /를 받아 이미 주입된 AppService를 호출 |
route, 요청 입력, HTTP 응답 계약이 바뀔 때 |
AppService |
재사용할 애플리케이션 로직을 제공하는 provider | getHello()가 'Hello World!' 문자열을 반환 |
계산·데이터 처리처럼 HTTP와 분리할 로직이 바뀔 때 |
main.ts- 책임 애플리케이션을 부트스트랩하고 HTTP listener를 시작한다.
- 기여
NestFactory.create(AppModule)로 부트스트랩하고 설정된 port에서 요청을 기다린다. - 변경 전역 pipe·prefix, HTTP adapter, port의 시작 경계가 바뀔 때 수정한다.
AppModule- 책임 루트 모듈의 구성 메타데이터를 소유한다.
- 기여 AppController와 AppService를 DI 컨테이너에 등록한다.
- 변경 module import나 controller·provider 등록 경계가 바뀔 때 수정한다.
AppController- 책임 HTTP 경로와 입력·응답 계약을 소유한다.
- 기여
GET /를 받아 주입된 AppService를 호출한다. - 변경 route나 HTTP 계약이 바뀔 때 수정한다.
AppService- 책임 재사용할 애플리케이션 로직을 provider로 제공한다.
- 기여
getHello()가'Hello World!'문자열을 반환한다. - 변경 HTTP와 분리할 계산·데이터 처리 로직이 바뀔 때 수정한다.
@Injectable()은 컨테이너가 관리할 수 있는 metadata를 제공하며, 실제 주입에는 해당 provider가 현재 module scope에서 사용 가능해야 한다. 기본 provider scope는 singleton이고 request·transient scope는 명시적으로 선택한다.-
src/디렉토리: NestJS 애플리케이션의 모든 소스 코드가 위치하는 핵심 디렉토리입니다. 우리가 작성할 대부분의 코드는 이 안에 들어있습니다.main.ts: 애플리케이션의 진입점(Entry Point) 파일입니다.npm run start:dev는 Nest CLI를 watch 모드로 실행하고, Nest는 이 엔트리 파일을 기준으로 애플리케이션을 부트스트랩합니다. 여기서 NestJS 애플리케이션 인스턴스를 생성하고 시작합니다.app.module.ts: 애플리케이션의 루트 모듈(Root Module)입니다. NestJS 애플리케이션의 최상위 모듈이며, 다른 모든 모듈의 기반이 됩니다. 앞서 1장 1절에서 설명했듯이, 모듈은 컨트롤러와 프로바이더를 묶어주는 역할을 합니다.app.controller.ts:AppController를 정의하는 파일입니다./경로로 들어오는 HTTP GET 요청을 처리하고 Hello World! 문자열을 반환하는 로직을 담고 있습니다. 클라이언트의 요청을 받아들이는 역할을 합니다.app.service.ts:AppService를 정의하는 파일입니다. 컨트롤러에서 호출하여 실제 비즈니스 로직(여기서는 Hello World! 문자열을 반환하는 간단한 로직)을 처리하는 프로바이더의 한 종류입니다.
-
package.json: 프로젝트의 메타데이터와 의존성 패키지 목록, 실행 스크립트 등을 정의하는 파일입니다.npm install명령어를 실행하면 여기에 명시된 패키지들이 설치됩니다. -
tsconfig.json: TypeScript 컴파일러 옵션을 설정하는 파일입니다. NestJS는 TypeScript 기반이므로, 이 파일이 중요하게 사용됩니다.
NestJS 애플리케이션의 실행 흐름 추적하기
이제 listener가 사용하는 포트를 바꾸지 않았을 때 http://localhost:3000으로 접속하면 Hello World!가 어떻게 출력되는지, NestJS 내부의 동작 흐름을 따라가 보겠습니다.
main.ts 실행:
애플리케이션을 시작하면 npm run start:dev 명령어가 Nest CLI를 watch 모드로 실행하고, Nest는 엔트리 파일인 main.ts를 기준으로 애플리케이션을 부트스트랩합니다.
이 파일의 핵심 코드는 다음과 같습니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// NestJS 애플리케이션 인스턴스를 생성합니다.
// 여기서 AppModule은 애플리케이션의 루트 모듈입니다.
const app = await NestFactory.create(AppModule);
// PORT가 없으면 3000번 포트에서 수신 대기합니다.
await app.listen(process.env.PORT ?? 3000);
}
bootstrap(); // bootstrap 함수를 호출하여 애플리케이션을 시작합니다.NestFactory.create(AppModule)은 import한 AppModule 클래스와 @Module() 메타데이터를 부트스트랩 입력으로 사용합니다. app.listen(...)은 전달받은 포트에서 HTTP 요청을 수신 대기합니다.
현재 공식 TypeScript starter 저장소의 main.ts는 app.listen(3000)을 사용하고, 현재 공식 First Steps 문서는 환경에서 포트를 덮어쓸 수 있는 app.listen(process.env.PORT ?? 3000) 예를 보여줍니다. 이 차이는 버전이나 템플릿에 따라 달라질 수 있으므로, 실제 프로젝트의 main.ts와 실행 로그에 표시된 주소를 기준으로 확인해야 합니다.
AppModule 초기화:
main.ts에서 AppModule 클래스를 전달했으므로, NestJS는 이 클래스의 @Module() 메타데이터를 읽어 애플리케이션 그래프를 구성합니다.
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() 데코레이터 안의 controllers 배열에 AppController가 등록되어 있고, providers 배열에 AppService가 등록되어 있는 것을 볼 수 있습니다.
NestJS는 이 정보를 바탕으로 AppController와 AppService의 의존성을 해석합니다. 별도 scope를 지정하지 않은 기본 provider는 애플리케이션 생명 주기에 연결된 singleton으로 조립되고, controller 생성자에 주입됩니다.
요청 처리: AppController 동작:
listener 포트를 바꾸지 않았다면 이제 웹 브라우저가 http://localhost:3000으로 요청을 보냅니다.
이 요청은 NestJS 애플리케이션에 도달하고, 등록된 컨트롤러 중 해당 요청을 처리할 컨트롤러를 찾습니다.
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller() // 이 클래스가 컨트롤러임을 나타냅니다. 기본 경로는 '/' 입니다.
export class AppController {
// AppService를 주입받습니다. NestJS가 자동으로 AppService 인스턴스를 제공합니다.
constructor(private readonly appService: AppService) {}
@Get() // HTTP GET 요청, 경로가 '/'일 때 이 메서드가 실행됩니다.
getHello(): string {
// AppService의 getHello() 메서드를 호출하여 결과값을 반환합니다.
return this.appService.getHello();
}
}@Controller()에 경로 접두사를 지정하지 않았고, @Get()에도 경로가 없기 때문에 이 메서드는 GET / 요청에 매핑됩니다.
AppController 생성자를 보면 private readonly appService: AppService 코드가 있습니다.
이것이 NestJS 핵심 기능 중 하나인 의존성 주입(Dependency Injection)입니다.
AppService는 AppModule의 providers에 등록되어 있고 기본 scope를 사용하므로, NestJS는 보통 애플리케이션에서 공유하는 인스턴스를 조립해 AppController에 주입합니다. 따라서 controller가 new AppService()처럼 직접 인스턴스를 만들 필요가 없습니다.
getHello() 메서드는 주입받은 appService의 getHello() 메서드를 호출합니다.
비즈니스 로직 처리: AppService 동작:
AppController의 요청을 받은 AppService가 실행됩니다.
import { Injectable } from '@nestjs/common';
@Injectable() // 이 클래스가 프로바이더임을 나타냅니다. (다른 곳에 주입될 수 있음)
export class AppService {
getHello(): string {
return 'Hello World!'; // 간단하게 "Hello World!" 문자열을 반환합니다.
}
}@Injectable() 데코레이터는 이 클래스가 NestJS의 의존성 주입 시스템에서 관리될 수 있도록 메타데이터를 제공합니다. 실제 주입을 위해서는 이 provider가 현재 module scope에서 사용할 수 있도록 등록되거나 가져와져야 합니다.
getHello() 메서드는 단순히 'Hello World!' 문자열을 반환합니다.
응답 반환:
AppService.getHello()가 반환한 'Hello World!' 문자열은 AppController.getHello()로 돌아옵니다. Nest의 표준 응답 처리에서 문자열 같은 primitive 값은 JSON으로 직렬화하지 않고 값 그대로 HTTP 응답에 실어 클라이언트(웹 브라우저)에 전달합니다.
앞의 다이어그램에서 부트스트랩과 요청을 나눈 이유도 여기에 있습니다. 이 예제처럼 기본 scope를 사용하는 provider는 DI 컨테이너의 등록·생성·주입이 요청 처리 전에 완료되고, 실제 GET / 요청에서는 AppController가 이미 주입된 AppService를 사용합니다.
이번 절에서는 NestJS가 모듈, 컨트롤러, 프로바이더를 이용해 요청을 받고 응답을 반환하는 흐름을 정리했습니다.
이후 기능도 이 기본 구조 위에서 라우팅, 의존성, 서비스 로직을 확장하는 방식으로 구성됩니다.
다음 절에서는 NestJS의 핵심 개념인 의존성 주입(Dependency Injection)에 대해 좀 더 자세히 알아보겠습니다.