[1人1P] 4-1. 백엔드 개발 (NestJS 입문)

[1人1P] 4-1. 백엔드 개발 (NestJS 입문)
2025. 11. 09. 한강버스 망원 정거장

NestJS에 대하여

NestJS는 클라이언트가 아닌 서버 사이드 NodeJS를 위해 만들어진 프레임워크이다. Typescript를 이용해 타입 안전성을 보장한 서비스 운영이 가능하며 모듈형 아키텍처를 차용하여 손쉽게 확장이 가능한 것이 특징이다.

NestJS를 이용하여 직접 백엔드를 개발해본 경험은 없지만, 한국디지털미디어고등학교(디미고)에서 디미페이 개발팀, 디미고인 개발팀에 몸을 담고있는 나는 백엔드 담당은 아니지만 어께 너머로 NestJS를 접해볼 일이 많았다. 때문에 나는 NestJS를 이번 1인1프로젝트에 적용하여 백엔드 개발 입문을 시작해보았다.


NestJS 프로젝트의 구조

NestJS 프로젝트의 구조

NestJS 프로젝트는 다음과 같은 구조를 가진다. 하나씩 설명해보록 하겠다.

  • node_modules : nodejs 패키지 파일이 설치되는 공간이다.
  • src : 대부분의 주요 프로젝트 파일이 저장되는 공간이며, 가장 중요한 공간이다.
  • test : 프로그램 모듈의 단위테스트를 정의하는 공간이다.
  • package.json : 프로젝트의 기본 정보를 담고있는 파일이다. 프로젝트의 이름, 사용된 패키지의 종류와 버전 등이 담겨있다.
  • tsconfig.json : Typescript를 사용할 때 필요한 파일로, Typescript를 Javascript로 변환할 때에 사용되는 설정파일이다.

대부분의 NodeJS 기반 프로젝트와 얼추 비슷한 구조를 가지고 있다. 다만, E2E(End-To-End) 테스를 위한 코드 작성 공간이 별도로 마련되어있다는 것이 개인적으로는 참신하게 느껴졌다. SvelteKit과 같은 프론트 프레임워크도 E2E를 내장하고 있다고는 한다.

💡
여기서 E2E(End-To-End) 테스트란?

서비스의 통합된 구성(프론트-백-DB 등)이 초기 기획대로 작동하는지 확인하는 테스트이다.

OJ 서비스에서 제출 결과를 채점하듯 제공된 조건에서 의도된 출력값을 내놓는지 확인하는 단위 검사라고 생각하면 쉬울 것 같다.

src폴더로 깊게 들어가보자. app.~ 으로 된 파일들이 가장 먼저 눈에 띈다. 각각의 파일들은 다음과 같은 기능을 수행한다.

src/main.ts

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

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

app.module.ts

main.ts는 Nest 서비스 인스턴스를 생성하고 초기화하는 단계를 수행한다. 서버 실행 시 가장 먼저 진입하는 파일이라고 생각하면 된다. 해당 파일에서 AppModule을 불러와 서버 인스턴스를 만드는 모습을 확인할 수 있다. 해당 파일을 한 번 따라가보자.

src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.module.ts

app.module.ts는 애플리케이션의 루트 모듈이다. 모듈이란 Controller와 Service 등을 모두 포함하는 코드 묶음의 단위이다. 모든 사이트에 이동 가능한 주소를 적어둔 사이트맵이 있는 것 처럼 해당 단위 기능을 구성하는 여러개의 코드를 묶어주는 파일인 것이다.

이 파일에서 controller로 app.controller.ts를 불러오고, providers로 app.service.ts를 불러오는 모습을 볼 수 있다. 한 번 따라가보자.

src/app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

app.controller.ts

app.controller.ts는 service를 실행하는 곳이라 생각하면 된다. 웹서비스의 경로(엔드포인트)를 정의하고, 이에 대한 서비스 함수를 실행한다.

간단하게 확인해보면 @Get()을 통해 서버 루트에 Get 요청이 들어오면 service에 getHello 함수를 실행해서 결괏값을 반환해주는 모습을 확인할 수 있다.

src/app.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

app.service.ts

app.service.ts는 Controller에서 호출할 수 있도록 내부 메서드를 작성하는 곳이다. 현재는 'Hello World!'라는 문자열을 반환하는 getHello함수가 정의되어 있다.

Controller에서 루트에 대한 Get 요청을 받으면 서버는 이 함수를 실행하고 사용자에게 'Hello World!' 라는 문자열을 반환할 것이다.

이렇게 NestJS 서버의 기본적인 구성을 살펴보았다. 지금은 간단한 예제 앱 수준이라 적은 파일의 양을 갖고 있지만, NestJS는 무한한 확장성을 제공한다.

서비스의 단위 기능이 많아지면 해당 기능들에 대한 Module, Service, Controller를 따로 만들고 최상위 루트 Module이 단위 기능들에 대한 Module을 import 하면 무한한 확장이 가능한 것이다.

서버 실행

NestJS의 구조를 살펴보았다면, 실제 구동되는 모습을 보아야 할 것이다.

실행 방법은 매우 간단하다. npm start를 입력해주면 된다.

해당 명령어가 입력되면 npm은 package.json에서 해당되는 명령어를 찾아 실행한다.

다만, 필자는 package manager로 pnpm을 사용하고 있기 때문에 pnpm start를 입력해주도록 하겠다.

서버 실행 모습

이렇게 Node가 NestJS 서버를 잘 불러온 것을 알 수 있다. 앞서 설명했던 Module, Controller가 초기화되며 루트 경로에 대한 Get 요청 엔드포인트가 매핑된 것을 로그를 통해 확인할 수 있다. (개인적으로 NestJS의 로깅 스타일은 매우 이쁘다고 생각한다.)

브라우저를 통해 접속해보면 서버가 매우 잘 작동하는 것을 알 수 있다.

이렇게 오늘은 NestJS의 기본 구성과 작동 원리에 대해 탐구해보는 시간을 가져보았다. 이제 이 프로젝트에 살을 붙이며 많은 기능들을 만들어나갈 것이다.