Nest.jsnx/nestStructureMonorepo

nx/nest로 더욱 견고한 Nest.js 환경 구성하기

nx/nest를 활용하여 Nest.js 애플리케이션의 구조를 개선하고, 모노레포 환경에서의 개발 효율성을 높이는 방법을 설명합니다.

nx/nest로 더욱 견고한 Nest.js 환경 구성하기

안녕하세요. 트레블씨투비에서 개발팀 리드를 하고있는 김이준입니다.

 

Nest.js는 구조적인 개발 패턴과 모듈 시스템 덕분에 빠르게 애플리케이션을 확장할 수 있습니다.
Node 진영의 대표적인 장점은 높은 자유도로 인해 빠른 개발이 가능하다는 점입니다.
하지만 반대로 이 높은 자유도는 코드 일관성을 해치거나 퀄리티 저하로 이어질 수 있으며, 특히 협업 과정에서 그 문제가 더욱 나타날 확률이 높아집니다.
서비스 규모가 커질수록 코어 API, 어드민, 배치 서비스처럼 여러 애플리케이션이 공존하게 되고, 여기에 도메인 규칙이나 데이터베이스 접근 계층까지 포함되면 관리 복잡도는 기하급수적으로 증가합니다.

 

이 글에서는 이러한 문제를 해결하기 위해 nx/nest를 어떻게 사용하여 더욱 견고하게 환경을 구성할 수 있을지를 다뤄보겠습니다.

 
 

이 글에서 사용되는 코드 구조는 아래 예시를 따릅니다.

apps |-- admin-api |-- package.json |-- project.json |-- core-api |-- package.json |-- project.json ... libs |-- core-database |-- project.json |-- core-domain |-- project.json ... package.json ...

 
 
 

프로젝트 의존성 시각화 (nx graph)

서비스가 커지면 각 애플리케이션(core, admin, batch 등)과 공용 모듈(domain, database 등) 사이의 의존성이 복잡해집니다.
nx graph는 실제 코드 기반으로 의존성 그래프를 그려 불필요한 의존성이나 역방향 참조를 조기에 발견하고 구조를 개선할 수 있게 해줍니다.

 

먼저, 의존성 관계를 시각화 해보겠습니다.

yarn nx graph

 

nx graph 예시

 

nx graph로 시각화하면 프로젝트의 현재 의존성 방향을 한눈에 확인할 수 있습니다.
예를 들어, 역방향 참조를 금지하는 규칙이 있는 프로젝트에서 core-domaincore-api를 참조한다면 잘못된 코드가 있는 소리겠죠?

 
 

특정 모듈만 포커싱해서 흐름을 볼 수도 있습니다.

yarn nx graph --focus=core-api

nx graph - 특정 모듈만 선택해서 보기 예시

 

만약 도메인별로 모듈이 분리해 두었다면 특정 모듈에 의존이 과도하게 몰려있는지 확인할 수도 있고, 의도치 않은 경유(예를 들면 core-api -> core-database 계층을 건너뛴 참조)를 금지하는 규칙이 있다면 이런 부분도 그래프에서 조기에 확인해 개선할 수 있습니다.

 
 
 

독립 또는 통합 애플리케이션 실행 (nx serve)

서비스 특성 및 규모에 따라 애플리케이션을 하나의 인스턴스에서 통합적으로 실행해야 할 경우도 있고, 애플리케이션 별로 독립된 인스턴스에서 실행해야 할 수도 있습니다.
nx serve를 이용해서 각 애플리케이션을 독립적으로 실행하거나 동시에 띄울 수 있습니다.
애플리케이션별 프로젝트 설정이 분리되어 있어 실행 환경간 충돌을 최소화 할 수 있습니다.

 

각 앱의 project.json에 serve 타깃을 정의할 수 있습니다.

예시 : apps/core-api/project.json

{ "name": "core-api", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/core-api/src", "projectType": "application", "targets": { "build": { "executor": "nx:run-commands", "options": { "command": "webpack-cli build", "args": ["node-env=production"] } }, "serve": { "executor": "@nx/js:node", "options": { "buildTarget": "core-api:build" } } } }

 
 

단일 또는 병렬로 애플리케이션 실행하기

# 단일 실행 yarn nx serve core-api # 병렬 실행 yarn nx run-many -t serve --projects=core-api,admin-api --parallel --maxParallel=2 --watch # 로그 스트림 yarn nx run-many -t serve --projects=core-api,admin-api --parallel --output-style=stream

 

프로젝트를 진행하다보면 여러 애플리케이션을 띄워야 할 상황이 빈번하게 있습니다.
애플리케이션 개수가 많아짐에 따라 애플리케이션별 로그를 왔다갔다 하면서 보는것은 개발의 피로도를 많이 높이는 요소입니다.
이를 nx는 병렬로 실행할 수 있는 환경을 제공하여 개발의 편의성을 많이 높일 수 있습니다.

 

[병렬 실행 및 로그 예시] nx serve - 애플리케이션 병렬 실행 예시 1 nx serve - 애플리케이션 병렬 실행 예시 2

 

함께 실행한 애플리케이션들의 로그를 한 공간에서 볼 수 있습니다.
애플리케이션이 많이 분리된 환경일수록 스트림 로그로 한 화면에서 추적이 가능한 부분이 개발 피로도를 낮추는데 크게 도움을 줍니다.

 
 

환경별 옵션 적용

로컬환경, 개발환경, 스테이징환경, 프로덕션환경 등 여러 환경별로 옵션이 다르게 적용이 되어야하는 경우가 많습니다.
이런 경우도 serve에서 환경별 적용해야할 옵션을 미리 선언해두고 nx를 실행할 때 원하는 환경으로 실행을 시킬 수 있습니다.

{ "targets": { "build": { "configurations": { "local": { "env": { "NODE_ENV": "local" } }, "development": { "env": { "NODE_ENV": "development" } }, "production": { "env": { "NODE_ENV": "production" } } }, ... }, "serve": { "options": { "buildTarget": "core-api:build" }, "configurations": { "local": { "buildTarget": "core-api:build:local" }, "development": { "buildTarget": "core-api:build:development" }, "production": { "buildTarget": "core-api:build:production" } }, ... } } }
yarn nx serve core-api -c local yarn nx serve core-api -c development yarn nx serve core-api -c production

예제에서는 단순한 NODE_ENV를 분리했지만 복잡할수록 각 환경별로 명확하게 옵션을 파악할 수 있습니다.

 
 
 

역방향 참조 차단

레이어드 아키텍처에서 흔히 발생하는 문제가 하위 레이어가 상위 레이어를 참조하는 상황입니다.
예를 들어 domain 레이어에서 api 레이어를 참조하는 상황입니다. 이런 참조는 구조 추적을 어렵게 하고 유지보수를 힘들게 만들 수 있습니다.

 

enforce boundaries 규칙을 활용하여 코드레벨에서 레이어별 경계를 강제해보겠습니다.
(물론 상황에 따라 역방향 참조를 열어두는 것이 더 유연한 경우도 있습니다.)

 

apps/core-api/project.json

{ "name": "core-api", "tags": ["layer:api"] }

 

libs/core-domain/project.json

{ "name": "core-domain", "tags": ["layer:domain"] }

 

libs/core-database/project.json

{ "name": "core-database", "tags": ["layer:database"] }

 

각 애플리케이션과 라이브러리별로 tag를 지정합니다.
이 태그를 기반으로 레이어별로 접근할 수 있는 영역과 접근할 수 없는 영역을 제어합니다.

 

libs/core-domain/eslint.config.mjs

{ rules: { '@nx/enforce-module-boundaries': [ 'error', { depConstraints: [ { sourceTag: 'layer:domain', notDependOnLibsWithTags: ['layer:api'], onlyDependOnLibsWithTags: ['layer:database'], }, ], ... }, ], } }

위 코드에서는 domain레이어에서는 api레이어의 참조를 막으며, database레이어의 참조를 허용하는 모습입니다.

 

역방향 참조 차단 예시 1 역방향 참조 차단 예시 2

코드에서 보시는바와 같이 규칙을 위반한 참조를 하는 경우 에러를 발생시켜 코드레벨에서 차단시킬 수 있습니다.

 
 
 

변경 영향도, 캐싱 기반의 개발/빌드/테스트 최적화

일반적인 모노레포에서는 구조적으로 앱과 라이브러리를 관리할 수는 있지만, 변경된 코드가 어떤 애플리케이션에 영향을 주는지 자동으로 계산하기 어렵습니다.
또, 이미 빌드한 결과를 재사용하기 어려워 CI/CD 파이프라인에서 모든 애플리케이션을 매번 빌드/테스트해야 합니다.

 

nx는 affectedcache를 통해 이를 최적화합니다.
affected: 변경된 코드와 의존성 그래프를 분석해 필요한 대상만 빌드/테스트
cache: 동일한 입력에 대해서는 과거 실행 결과를 그대로 재사용

 
 

로컬 개발 환경

로컬에서 개발시 내가 작성한 코드가 어떤 애플리케이션, 라이브러리에 영향을 주는지 확인할 수 있습니다.

# 특정 브랜치를 기반으로 변경된 프로젝트 확인 yarn nx show projects --affected --base=your_branch --head=HEAD # 변경된 프로젝트만 lint, test, build 수행 yarn nx affected -t lint,test,build --base=your_branch --head=HEAD

모든 앱을 빌드/테스트 하지 않고 변경된 앱만 대상으로 빌드/테스트를 진행합니다.

 
 

CI (예시 : Github Actions)

jobs: build: steps: - name: Install dependencies run: yarn install --frozen-lockfile - name: Affected Lint, Test, Build run: | yarn nx affected -t lint,test,build --base=your_remote_branch --head=HEAD ...

PR/브랜치에서 변경된 대상만 빌드/테스트하여 CI 시간을 단축시킬 수 있습니다.

 
 

CD (예시 : Github Actions)

- name: Find affected apps id: affected run: | APPS=$(yarn nx show projects --affected --type=app --base=your_remote_branch --head=HEAD) echo "apps=$APPS" >> $GITHUB_OUTPUT - name: Build affected apps run: | yarn nx affected -t build --base=your_remote_branch --head=HEAD

모든 애플리케이션을 배포하는것이 아닌, 변경이 있는 애플리케이션만 배포합니다.
core-domain과 같은 라이브라리만 수정해도 이를 참조하고있는 애플리케이션도 자동으로 포함되어 빌드됩니다.

 

nx의 캐시 매커니즘은 같은 input이면 이전의 output을 재 사용하는 방식이예요.
nx에는 로컬캐시와 원격 캐시가 있어요.
로컬캐시는 로컬 환경에서 이전에 실행결과를 .nx/cache에서 재사용해요.
원격캐시(nx cloud 등)는 팀원과 결과를 공유해서 빌드, 테스트 속도를 단축할 수 있어요.
serve는 롱러닝 타겟이라 캐시 대상이 아니예요.

 
 
 
 

마치며

이번 글에서는 nx/nest를 기반으로 환경을 구성하면서 얻을 수 있는 장점들을 살펴보았습니다.
단순히 코드를 관리한다는 수준이 아닌 협업 과정에서 발생할 수 있는 복잡성, 비효율성을 구조적으로 잡아줄 수 있는 역할을 합니다.

 

글에서 다루지는 않았지만 패키지 분리, 코드 생성 스캐폴딩 등과 같은 기능들을 제공해 코드 전반에 일관성을 만들어줍니다.
신규 팀원이 합류했을 때도 구조가 명확하고 규칙이 강제되기 때문에 온보딩 시간을 단축할 수 있고, 코드리뷰 품질을 향상시켜 서비스 안정성까지 확보할 수 있죠.

 

다만 이런 구조가 언제나 필요하다고 생각하지는 않습니다.
특히 스타트업 환경에서는 MVP를 빠르게 만들어야하거나 서비스가 만들어졌지만 트래픽이 거의 없는 상황이 많습니다.
이런 경우 단일 애플리케이션 구조를 선택하는게 더 합리적일 수 있습니다.
서비스와 회사가 성장하면서 소프트웨어도 함께 성장시키는 방법이 더 합리적이라고 생각합니다. (Yarn Workspaces, Lerna 같은 툴도 필요에 따라 좋은 선택지가 될 수 있습니다.)