첫 번째 NestJS 애플리케이션 만들기
Nest CLI가 만든 핵심 파일의 책임을 구분하고 부트스트랩부터 컨트롤러 응답까지 첫 요청의 흐름을 추적합니다.
NestJS 개발 환경 구성이 완료되었습니다.
앞서 생성하고 실행해 본 Hello World! 애플리케이션은 NestJS의 가장 기본적인 형태를 보여줬습니다.
이번 절에서는 이 애플리케이션의 기본 구조를 자세히 살펴보고, Hello World! 메시지가 화면에 나타나는 과정, 즉 NestJS가 요청을 처리하는 흐름을 함께 파헤쳐 보겠습니다.
Nest 부트스트랩과 첫 요청의 두 단계
DI 부트스트랩으로 provider를 조립한 뒤 첫 GET이 이미 주입된 service를 쓰는 두 시간 경계를 보여 준다.
Nest 첫 요청 · 시작과 요청
기본 provider는 시작할 때 주입되고, GET 요청에서 이미 주입된 Service를 사용합니다
이 Hello World 예제는 기본 scope를 사용합니다. AppModule은 DI 조립 정보이고 요청이 매번 거치는 단계가 아니며, 문자열은 JSON 객체로 바꾸지 않고 응답합니다.
요청 전 · 부트스트랩
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 응답으로 전송된다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다.
- 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 36 | 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| module 메타데이터 main이 AppModule과 메타데이터를 Nest에 넘깁니다. | 입력: AppModule · 시점: 부트스트랩 | 조립할 그래프가 전달됩니다. |
| provider 조립·주입 DI가 기본 scope provider를 만들어 controller에 주입합니다. | 산출물: 주입된 AppService · 요청: 아직 없음 | 첫 요청 전에 협력자가 준비됩니다. |
| GET의 문자열 응답 GET이 controller·이미 주입한 service를 지나갑니다. | 응답: Hello World! · service: 이미 주입됨 | 요청마다 조립하지 않고 업무를 실행합니다. |
Nest 부트스트랩과 첫 요청의 두 단계
DI 부트스트랩으로 provider를 조립한 뒤 첫 GET이 이미 주입된 service를 쓰는 두 시간 경계를 보여 준다.
Nest 첫 요청 · 시작과 요청
기본 provider는 시작할 때 주입되고, GET 요청에서 이미 주입된 Service를 사용합니다
이 Hello World 예제는 기본 scope를 사용합니다. AppModule은 DI 조립 정보이고 요청이 매번 거치는 단계가 아니며, 문자열은 JSON 객체로 바꾸지 않고 응답합니다.
요청 전 · 부트스트랩
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 응답으로 전송된다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 부트스트랩 단계에서 main.ts가 AppModule 클래스와 모듈 메타데이터를 Nest에 전달하면 DI 컨테이너가 기본 범위 provider를 조립해 controller에 주입한다. 이후 요청 단계에서 GET 루트 요청이 AppController와 이미 주입된 AppService를 지나 Hello World 문자열 그대로 HTTP 응답이 된다.
- 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 36 | 시작할 때 main.ts와 AppModule 메타데이터로 기본 범위 프로바이더를 조립하여 컨트롤러에 주입한다. 각 요청은 컨트롤러와 이미 주입된 서비스를 거쳐 처리된 뒤 문자열 응답으로 반환된다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| module 메타데이터 main이 AppModule과 메타데이터를 Nest에 넘깁니다. | 입력: AppModule · 시점: 부트스트랩 | 조립할 그래프가 전달됩니다. |
| provider 조립·주입 DI가 기본 scope provider를 만들어 controller에 주입합니다. | 산출물: 주입된 AppService · 요청: 아직 없음 | 첫 요청 전에 협력자가 준비됩니다. |
| GET의 문자열 응답 GET이 controller·이미 주입한 service를 지나갑니다. | 응답: Hello World! · service: 이미 주입됨 | 요청마다 조립하지 않고 업무를 실행합니다. |
프로젝트 기본 구조 이해하기
NestJS CLI를 통해 nest new 명령어로 프로젝트를 생성하면 다음과 같은 디렉토리 및 파일 구조를 갖게 됩니다.
모든 파일을 다 살펴보기보다는 지금 당장 우리가 이해해야 할 핵심적인 파일들을 중심으로 살펴보겠습니다.
Nest 핵심 파일 · 책임과 변경
main.ts는 시작 설정, Module은 조립, Controller는 HTTP, Service는 로직을 소유합니다
Hello World 요청에 기여하는 일과 변경할 이유를 함께 비교합니다. Injectable 메타데이터만으로 주입이 성립하지 않으며 해당 module scope에서 provider가 사용 가능해야 합니다.
main.ts
NestFactory.create(AppModule)로 부트스트랩하고 설정된 port를 listenAppModule
AppController
GET /를 받아 이미 주입된 AppService를 호출AppService
getHello()가 'Hello World!' 문자열을 반환@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를 정의하는 파일입니다.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 저장소와 First Steps 문서는 모두 환경에서 포트를 덮어쓸 수 있는 app.listen(process.env.PORT ?? 3000) 예를 보여줍니다. 이 절의 코드 표기는 Nest 11의 CommonJS 구성을 기준으로 하므로 최신 템플릿의 import 확장자나 설정과 다를 수 있습니다. 실제 프로젝트의 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를 사용합니다.
응답을 바꾸며 요청 경로 확인하기
- 실행 중인 첫 앱에서
AppService.getHello()의 반환 문자열만'Hello Nest!'로 바꿉니다. 감시 모드의 재컴파일이 끝난 뒤GET /를 다시 요청하면 새 문자열이 나와야 합니다. 컨트롤러는 같은 메서드를 호출하므로 수정할 필요가 없습니다. - 그다음 컨트롤러의
@Get()을@Get('hello')로 바꿉니다. 전역 접두사가 없다는 전제에서 이제GET /hello가 새 문자열을 반환합니다. 다른 루트 핸들러가 없다면GET /는 404가 됩니다. 문자열은 그대로인데 요청 경로만 달라진 것입니다. - 다음 절의 기본 상태와 비교할 수 있도록 두 변경을 원래 코드로 되돌립니다.
getHello라는 메서드 이름이 URL을 정하지는 않습니다. 경로는 데코레이터가, 반환 값은 메서드가 결정합니다. 실제 실행 로그나 응답이 예상과 다르면 저장한 파일과 감시 재컴파일 결과를 먼저 확인하세요.
코드를 보지 않고 예상해 보기
전역 접두사가 없는 같은 앱에서 @Controller('welcome'), @Get('message')로 바꾸고 서비스는 원래 문자열을 반환한다고 가정합니다. 요청할 HTTP 메서드·경로와 예상 응답을 써 보세요. 이어 요청을 두 번 보낼 때 AppModule을 매번 새로 만드는지도 설명해 보세요.
답과 연결되는 코드
GET /welcome/message가 Hello World! 문자열을 반환합니다. 컨트롤러 접두사와 핸들러 경로를 합쳐 URL을 정하고, 서비스 반환 값을 응답에 사용하기 때문입니다. 이 예제의 기본 범위에서는 앱 시작 때 조립한 컨트롤러와 프로바이더를 재사용합니다. AppModule은 요청마다 호출하는 처리 단계가 아닙니다.
이번 절에서는 NestJS가 모듈, 컨트롤러, 프로바이더를 이용해 요청을 받고 응답을 반환하는 흐름을 정리했습니다.
이후 기능도 이 기본 구조 위에서 라우팅, 의존성, 서비스 로직을 확장하는 방식으로 구성됩니다.
다음 절에서는 같은 요청 흐름을 사용자 목록 예제로 넓혀 모듈, 컨트롤러, 서비스의 기본 책임을 구분합니다. 의존성 주입과 IoC 컨테이너는 그다음 장에서 더 깊이 다룹니다.
등록이 빠졌을 때는 실패 시점부터 구분하기
아래 비교는 기본 scope, 이 장의 클래스와 생성자, 정상적인 포트 설정을 사용하며 다른 모듈에서 AppService를 제공하지 않는 경우입니다. TypeScript의 import는 코드를 참조하게 하고, @Module()의 배열은 Nest가 관리할 대상을 등록합니다. 파일을 가져왔다는 사실만으로 provider나 HTTP handler가 등록되지는 않습니다.
| AppModule의 변경 | 시작 과정 | 첫 요청에서 확인할 결과 |
|---|---|---|
| 원래의 controllers와 providers 유지 | AppService를 만들고 AppController에 주입한 뒤 listen | GET /에서 Hello World! |
| controllers는 유지하고 providers에서 AppService 제거 | AppController의 의존성을 해결하지 못해 bootstrap 실패 | listener가 정상적으로 시작되기 전의 문제 |
| controllers에서 AppController만 제거 | 남은 구성은 시작할 수 있음 | 다른 handler가 없다면 GET /은 404 |
등록은 유지하고 요청만 POST /로 변경 | 정상 시작 | GET handler와 HTTP method가 달라 404 |
첫 번째 실패는 “서비스가 등록되지 않아 controller를 조립하지 못함”, 두 번째 실패는 “요청과 일치하는 controller handler가 없음”입니다. 어느 파일의 코드를 열었는지보다 시작 로그와 실제 요청 method·path를 함께 확인하면 원인을 좁힐 수 있습니다.
조건을 바꿔 확인하기
연습문제: import와 provider 등록
AppController가 AppService를 생성자로 요구합니다. 두 파일의 import와 @Injectable()은 유지했지만 providers: []로 바꾸었고, 다른 모듈도 AppService를 제공하지 않습니다. 첫 GET 요청에서만 서비스 오류가 날까요?
풀이: 기본 scope에서는 요청 전 bootstrap 중에 AppController를 만들며 의존성을 해결합니다. AppService provider가 없으므로 이 단계에서 실패합니다. import와 decorator를 유지해도 provider 등록을 대신하지 못합니다. 원래의 providers: [AppService]를 복원한 뒤 시작 로그부터 확인합니다.
연습문제: controller를 등록하지 않은 경우
providers: [AppService]는 그대로 두고 controllers: []로 바꾸었습니다. 다른 controller와 middleware가 없고 listener는 정상 시작했습니다. AppService에 getHello()가 존재하면 GET /에 자동으로 응답할까요?
풀이: 응답하지 않습니다. 서비스의 메서드는 HTTP route가 아니며, 등록된 AppController의 @Get() handler가 그 메서드를 호출해야 합니다. 이 조건에서는 일치하는 handler가 없어 404가 됩니다. controller를 등록하면 원래의 연결이 복원됩니다.
연습문제: 메서드 이름과 요청 계약
원래의 @Controller()와 @Get()은 유지하면서 controller와 service의 메서드 이름만 서로 일치하도록 welcome()으로 바꾸었습니다. 전역 prefix는 없고 다른 handler도 없습니다. GET /, GET /welcome, POST / 중 어느 요청이 이 handler를 선택할까요?
풀이: GET /만 선택합니다. HTTP method와 path는 decorator가 정합니다. 메서드 이름을 바꾸어도 @Get()의 빈 경로는 루트 경로이며, GET /welcome과 POST /은 이 handler와 일치하지 않습니다. 경로를 바꾸려면 @Get('welcome')처럼 decorator 인자를 변경해야 합니다.