NestJS 처음 시작하기
최근에 NestJS로 시작하는 백엔드(backend) 프로젝트가 부쩍 많아진 느낌입니다. 이번 포스팅에서는 차세대 웹 프레임워크로 각광받고 있는 NestJS에 대해서 함께 알아보려고 합니다.
NestJS란?
NestJS는 자바스크립트나 타입스크립트로 서버 애플리케이션을 개발할 수 있는 백엔드 웹 프레임워크(Web framework)입니다. 다른 프로그래밍 언어에서 넘어오신 분이라면 자바의 스프링(Spring)이나 파이썬의 장고(Django)를 생각하시면 이해가 쉬우실 것 같네요.
자바스크립트 쪽에서는 상당히 오랫동안 Express라는 웹 프레임워크가 서버 애플리케이션 개발에 있어서 압도적인 점유율을 차지했었는데요. Express가 워낙 경량화된 프레임워크여서 정말 핵심적인 기능만 제공하다보니 간단한 서버 애플리케이션을 개발하는데는 큰 문제가 없었지만, 어느 정도 규모가 있는 프로젝트에서는 직접 구현해야하는 기능이 너무 많고 다른 라이브러리를 추가로 필요로 하는 경우도 많아서 불편했었습니다.
이러한 문제를 해결하기위해서 등장한 것이 NestJS라는 프레임워크인데요. NestJS는 기업용 애플리케이션을 개발하기에도 무리가 없을 정도로 왠만한 기능은 내장하고 있고 플러그인(plugin)을 통해서 쉽게 확장도 할 수 있습니다. 뿐만 아니라 OOP(객체 지향 프로그래밍), DI(의존성 주입), AOP(과점 지향 프로그래밍)와 같은 백엔드 개발 트랜드 충실히 반영하고 있기 때문에 고품질의 코드를 작성하는데도 도움을 주는 것으로 알려져있습니다.
참고로 많은 분들이 이름이 비슷해서 NestJS를 NextJS로 착각하시곤 하는데요. NextJS는 리액트(React) 기반 SSR 프레임워크이며, NestJS는 주로 서버 사이드 애플리케이션이나 백엔드(backend) API를 개발할 때 사용되오니 착오없으시길 바라겠습니다.
SSR(Server-side rendering)에 대해서는 관련 포스팅에서 자세히 다루고 있으니 참고 바랍니다.
그럼 서론은 여기서 줄이고 간단한 실습을 통해서 NestJS를 어떻게 시작할 수 있는지 알아볼까요? 🏇
NestJS CLI 설치
NestJS는 개발자가 좀 더 편리하게 NestJS 프로젝트를 개발하고 설정할 수 있도록 강력한 CLI(명령 줄 인터페이스) 도구를 제공하고 있습니다. 따라서 NestJS CLI를 통해서 NestJS를 시작하시는 것을 강력하게 추천드리고 싶습니다.
그럼 터미널을 열고 다음 명령어를 실행하여 NestJS CLI 도구를 전역(global)에 설치해보겠습니다.
$ npm i -g @nestjs/cli
added 251 packages, and audited 252 packages in 11s
41 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
이제 터미널에서 nest
라는 명령어를 사용할 수 있는데요.
단순히 nest
명령어를 실행해보면 간단한 설명서를 보실 수 있으실 겁니다.
$ nest
nest
Usage: nest <command> [options]
Options:
-v, --version Output the current version.
-h, --help Output usage information.
Commands:
new|n [options] [name] Generate Nest application.
build [options] [app] Build Nest application.
start [options] [app] Run Nest application.
info|i Display Nest project details.
add [options] <library> Adds support for an external library to your project.
generate|g [options] <schematic> [name] [path] Generate a Nest element.
Schematics available on @nestjs/schematics collection:
┌───────────────┬─────────────┬──────────────────────────────────────────────┐
│ name │ alias │ description │
│ application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ itc │ Generate an interceptor declaration │
│ interface │ itf │ Generate an interface │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ service │ s │ Generate a service declaration │
│ library │ lib │ Generate a new library within a monorepo │
│ sub-app │ app │ Generate a new application within a monorepo │
│ resource │ res │ Generate a new CRUD resource │
└───────────────┴─────────────┴──────────────────────────────────────────────┘
NestJS 프로젝트 구성
다음으로 NestJS CLI를 이용해서 새로운 NestJS 프로젝트를 구성해보겠습니다.
nest new
명령어 뒤에 프로젝트 명을 넘기면 해당 이름의 디렉토리가 생기고 그 안에 NestJS 프로젝트가 자등으로 구성이 될 것입니다.
저는 our-nestjs
를 프로젝트 이름으로 사용할께요.
$ nest new our-nestjs
⚡ We will scaffold your app in a few seconds..
? Which package manager would you ❤️ to use? (Use arrow keys)
❯ npm
yarn
pnpm
명령어를 실행하면 자바스크립트 패키지 매니저로 무엇을 사용할 건지 물어볼건데요 저는 그냥 Node.js에 기본 내장된 npm을 사용할께요. 여러분은 원하시는 옵션을 선택하시면 됩니다.
⚡ We will scaffold your app in a few seconds..
? Which package manager would you ❤️ to use? npm
CREATE our-nestjs/.eslintrc.js (663 bytes)
CREATE our-nestjs/.prettierrc (51 bytes)
CREATE our-nestjs/README.md (3340 bytes)
CREATE our-nestjs/nest-cli.json (171 bytes)
CREATE our-nestjs/package.json (1941 bytes)
CREATE our-nestjs/tsconfig.build.json (97 bytes)
CREATE our-nestjs/tsconfig.json (546 bytes)
CREATE our-nestjs/src/app.controller.spec.ts (617 bytes)
CREATE our-nestjs/src/app.controller.ts (274 bytes)
CREATE our-nestjs/src/app.module.ts (249 bytes)
CREATE our-nestjs/src/app.service.ts (142 bytes)
CREATE our-nestjs/src/main.ts (208 bytes)
CREATE our-nestjs/test/app.e2e-spec.ts (630 bytes)
CREATE our-nestjs/test/jest-e2e.json (183 bytes)
✔ Installation in progress... ☕
🚀 Successfully created project our-nestjs
👉 Get started with the following commands:
$ cd our-nestjs
$ npm run start
Thanks for installing Nest 🙏
Please consider donating to our open collective
to help us maintain this package.
🍷 Donate: https://opencollective.com/nest
그러면 프로젝트 디렉토리에 여러 가지 파일이 자동으로 생성되는 것을 볼 수 있으실 거에요. 정말 편하죠? 😁
NestJS 애플리케이션 구동
이제 시키는대로 프로젝트 디렉토리로 들어가서 npm run start
명령어를 실행해볼까요?
$ cd our-nestjs
$ npm run start
> our-nestjs@0.0.1 start
> nest start
[Nest] 23176 - 2022-12-31, 4:23:39 p.m. LOG [NestFactory] Starting Nest application...
[Nest] 23176 - 2022-12-31, 4:23:39 p.m. LOG [InstanceLoader] AppModule dependencies initialized +16ms
[Nest] 23176 - 2022-12-31, 4:23:39 p.m. LOG [RoutesResolver] AppController {/}: +2ms
[Nest] 23176 - 2022-12-31, 4:23:39 p.m. LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 23176 - 2022-12-31, 4:23:39 p.m. LOG [NestApplication] Nest application successfully started +0ms
그러면 NestJS 애플케이션이 구동되는 것을 볼 수 있는데요.
다른 터미널창을 열고 curl
명령어로 http://localhost:3000
을 찔러보면 Hello World!
가 응답되는 것을 확인할 수 있으실 겁니다.
$ curl http://localhost:3000
Hello World!%
main.ts
자동으로 생성된 파일 중에서 제일 먼저 살펴볼 파일은 src
디렉토리 안에 있는 main.ts
파일입니다.
이 파일은 NestJS 애플리케이션이 시작되는 진입 지점(entry point)이 되는데요.
파일을 열어보면 매우 짧은 코드가 들어있습니다.
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
코드의 마지막 줄에는 결국 bootstrap()
이라는 함수를 호출하고 있는데요.
bootstrap()
함수 안에서는 app.module
파일로 부터 AppModule
를 불러와서 NestFactory
가 애플리케이션을 객체를 생성하고 3000 포트로 HTTP 요청을 받고 있습니다.
이것이 아까 전에 위에서 http://localhost:3000
에 접속이 가능했던 이유입니다.
모듈(Module)
main.ts
파일에서 불러오고 있는 app.module.ts
파일을 열어보면 AppModule
클래스를 찾을 수 있습니다.
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()
이라는 데코레이터(decorator)가 호출되고 있는데요.
데코레이터는 기존에 Angular를 써보신 분이라면 익숙하시겠지만 자바스크립트에서는 비교적 새로운 문법이기 때문에 생소하게 느끼는 분들도 많을 것 같습니다.
NestJS에서 데코레이터는 일반적으로 클래스나 메서드에 어떤 정보를 추가해줄 때 많이 활용이 되고 있는데요. 파이썬에서도 비슷한 데코레이터 문법이 있고, 자바에서는 어노테이션(annotation)이라고 부르는 문법 요소랑 비슷하다고 생각하시면 됩니다. 궁금하신 분들은 추가로 검색을 해보시기를 추천드릴게요.
아무튼 @Module()
데코레이터는 imports
, controllers
, providers
속성으로 이루어진 객체를 인자로 받는데요.
controllers
속성에는 HTTP 요청을 받아서 응답을 보내는 컨트롤러 클래스를 나열해줄 수 있고요.
providers
속성에는 컨트롤러가 사용하는 다양한 일반 클래스(주로 서비스 클래스)를 나열해줄 수 있습니다.
여기서는 비어있는 imports
속성에는 해당 모듈이 의존하고 있는 다른 모듈을 나열해줄 수 있습니다.
모듈(module)은 NestJS에 매우 중요한 개념이라서 잘 이해하고 있어야 하는데요. 하나의 NestJS 애플리케이션은 보통 여러 개의 모듈로 이루어지는데 기능 단위로 애플리케이션을 쪼개놓은 단위라로 생각할 수 있습니다.
여기서 중요한 것은 모듈은 서로 의존할 수 있다는 것인데요.
바로 @Module()
데코레이터에 인자로 넘기는 객체의 imports
속성을 통해서 이 의존 관계를 명시하도록 되었습니다.
nestjs new
명령어로 NestJS 프로젝트를 생성하면 기본적으로 최상위 모듈인 AppModule
하나 밖에 없지만,
프로젝트 규모가 점점 커지게 되면 다른 모듈을 작성한 후 AppModule
이 불러올 수 있도록 @Module()
데코레이터를 호출할 때 imports
속성을 사용하게 됩니다.
정리하면 NestJS는 일종의 IoC(Inversion of Control) 컨테이너의 역할을 하면서 여러 모듈을 DI(의존성 주입)을 통해서 엮어준다고 보시면 됩니다.
어떻게 엮어야 하는지는 개발자가 각 모듈에 @Module()
데코레이터의 imports
속성으로 NestJS에 알려줘야 하고요.
컨트롤러(Controller)
다음으로 NestJS에서 하나의 축을 담당하고 있는 컨트롤러에 대해서 알아보겠습니다.
컨트롤러는 HTTP 요청을 받아서 처리하고 응답을 해주는 역할을 담하고 있는 클래스인데요.
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();
}
}
위와 같이 클래스 위에다가 @Controller()
데코레이터를 호출해주면 NestJS가 해당 클래스는 컨트롤러로 인식을 하게 되는데요.
클래스 내의 각 메서드에는 @Get()
, @Post()
, @Delete()
와 같은 HTTP 방식(method)에 해당하는 데코레이터를 붙여주게 됩니다.
또한 이러한 데코레이터들은 URL 경로를 나타내는 문자열을 인자로 받는데요. NestJS는 데코레이터로 명시된 HTTP 방식과 URL 경로를 기준으로 부합하는 클래스의 메서드를 호출해줍니다.
예를 들어, @Controller("aaa")
가 붙어있는 클래스의 @Post("bbb")
가 붙어있는 메서드가 있었다면,
POST 방식으로 http://localhost:3000/aaa/bbb
을 찔렀을 대 해당 메소드가 호출되었을 것입니다.
이를 통해 아까 전에 위에서 http://localhost:3000
을 찔렀을 때, AppController
클래스의 getHello()
함수가 이를 받아서 Hello World!
라는 응답을 해줬다는 것을 알 수 있습니다.
서비스(Service)
마지막으로 살펴볼 서비스 클래스는 일반적으로 비지니스 로직을 수행하는 역할을 담당합니다.
src
디렉토리 안에 있는 app.service.ts
를 열오보면 AppController
클래스가 사용하고 있던 AppService
클래스를 확인할 수 있는데요.
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return "Hello World!";
}
}
이 클래스 위에는 @Injectable()
데코레이터가 사용되고 있죠?
@Injectable()
데코레이터가 붙어있는 클래스는 NestJS가 인스턴스를 생성하여 다른 클래스에 생성자를 통해서 주입을 해줄 수 있습니다.
위에서 AppModule
위에서 @Module()
데코레이터를 호출할 때 providers
속성에 AppService
클래스를 명시해줬었죠?
그렇게 때문에 AppController
클래스의 생성자의 인자로 AppService
클래스의 인스턴스가 주입이 되었고,
AppController
클래스의 getHello
메서드 내애서 AppService
클래스의 getHello
메서드를 호출할 수 있었던 것입니다.
이렇게 컨트롤러의 역할과 서비스의 역할을 분리함으로써 좀 더 유지보수가 용이한 애플리케이션을 개발할 수가 있는 것입니다.
마치면서
지금까지 간단한 실습을 통해서 NestJS 프로젝트를 시작하고 관련된 핵심 개념에 대해서 살펴보았습니다. 기존에 스프링이나 장고와 같은 웹 프레임워크를 써보셨다면 특별히 새로울 것이 없겠지만, 이러한 MVC 개념을 처음 접하시는 분들에게는 조금 어려운 내용이 되었을 것 같기도 합니다.
본 포스팅에서 다룬 부분은 사실 NestJS가 제공하는 방대한 기능의 빙산의 일각이라고 볼 수 있는데요. 추후 다른 포스팅을 통해서 NestJS의 여러 가지 기능들을 좀 더 깊게 알아보는 시간을 갖도록 하겠습니다.
NestJS 관련 포스팅은 NestJS 태그를 통해서 쉽게 만나보세요!