NestJS와 Nx를 활용한 모노레포 아키텍처 구축하기

NestJS와 Nx를 활용한 모노레포 아키텍처 구축하기

1. 소개

Monorepo란?

모노레포는 단일 저장소에 여러 프로젝트를 함께 관리하는 개발전략입니다. 하나의 git 저장소에 여러 어플리케이션과 라이브러리들을 저장합니다.

Monorepo의 장점

  1. 코드 재사용

auth, db 라이브러리를 선언하여, 모든 어플리케이션에서 이를 사용할 수 있습니다. 일반적인 방식에서는 패키지를 발행하고 버전관리를 하는 노력이 필요하지만 모노레포에서는 이 과정없이 코드를 공유할 수 있습니다.

  1. 원자적 변경

예를 들어, 인증 로직을 변경해야 할 때 인증로직을 사용하는 어플리케이션에 대해서만 변경을 적용할 수 있습니다.

  1. 일관된 개발환경

모든 프로젝트가 동일한 린트, 빌드, 테스트 환경을 공유할 수 있습니다.

  1. 의존성관리 최소화

모든 프로젝트가 node_modules 를 공유하므로 패키지 중복을 줄이고 버전 충돌을 방지할 수 있습니다.

nestjs에서 nx를 사용하여 모노레포를 관리한 이유

  1. nx는 nestjs를 위한 스캐폴딩 도구를 제공합니다.
npx nx generate @nx/nest:library --name=auth --directory=libs/auth
  1. 프로젝트 그래프 시각화

다음과 같이 의존관계를 갖는 라이브러리들과 어플리케이션의 그래프를 시각화 할 수 있습니다.

nx-graph

  1. 캐싱

린트, 빌드, 테스트를 캐싱하여 어플리케이션 규모가 커질때 더 빠른 속도로 개발을 할 수 있도록 도와줍니다.

  1. 마이크로 서비스 지원

다음과 같이 여러 어플리케이션을 동시에 실행하고 디버깅할 수 있는 도구를 제공합니다.

npx nx run-many --target=serve --projects=user-web,admin-dashboard,payment-service,inventory-service --parallel

2. Nx 설정파일 분석 (nx.json / tsconfig.base.json)

nx.json

nx.json은 Nx 워크스페이스의 핵심 설정 파일로, 프로젝트별 메타데이터와 캐시 전략, 의존성 계산 방식 등을 정의합니다. 실제 빌드/테스트/실행 설정은 각 프로젝트의 project.json 또는 workspace.json, 그리고 tsconfig.*.json, jest.config.ts 등에 분산되어 있습니다.


namedInputs

namedInputs는 Nx가 변경 감지 및 캐시 유효성 판단 시 어떤 파일을 기준으로 판단할지 정의하는 설정입니다. 이는 targetDefaults에서 어떤 입력을 사용할지 지정할 때도 함께 사용됩니다.

"namedInputs": {
  "default": ["{projectRoot}/**/*", "sharedGlobals"],
  "production": [
    "default",
    "!{projectRoot}/.eslintrc.json",
    "!{projectRoot}/eslint.config.mjs",
    "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)"
  ],
  "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
}
  • default: 프로젝트 루트의 전체 파일({projectRoot}/*/)과 전역 설정 파일(sharedGlobals)을 포함합니다. 대부분의 기본 타겟(build, test, lint)이 이 입력을 기준으로 실행됩니다.
  • production: default를 포함하되, 테스트/린트와 관련된 파일을 제외합니다. 프로덕션 빌드 타겟에 사용됩니다.
  • sharedGlobals: 워크스페이스 전역에 영향을 미치는 파일들(예: CI 설정 파일 등)을 정의합니다.

plugin

nx.jsonplugin 필드는 워크스페이스에 적용할 nx plugin들과 그 설정을 정의하는 곳입니다.

"plugins": [
  {
    "plugin": "@nx/js/typescript",
    "options": {
      "typecheck": {
        "targetName": "typecheck"
      },
      "build": {
        "targetName": "build",
        "configName": "tsconfig.lib.json",
        "buildDepsName": "build-deps",
        "watchDepsName": "watch-deps"
      }
    },
    "exclude": ["libs/utils/*", "libs/db/*", "libs/auth/*", "libs/payment/*", "libs/inventory/*"]
  }
]
plugin key
  1. plugin : 적용할 nx plugin을 정의한 곳입니다.
  2. option : plugin의 세부 설정을 정의한 곳 입니다.
  3. exclude : plugin을 적용하지 않을 프로젝트의 목록입니다.
주요 plugin 설명
  • @nx/js/typescript : TypeScript 프로젝트에 타입 체크 및 빌드 타겟을 자동으로 생성해주는 플러그인입니다.
  • @nx/webpack/plugin : Webpack 기반 웹 애플리케이션의 빌드, 서브, 프리뷰 타겟을 자동 구성합니다.
  • @nx/eslint/plugin : ESLint 린트 타겟을 각 프로젝트에 자동으로 추가해주는 플러그인입니다.
  • @nx/jest/plugin : Jest 기반 단위 및 E2E 테스트 타겟을 프로젝트에 자동으로 설정합니다.
동작 방식
  • project.json이 존재하지 않더라도, Nx는 plugins 설정을 기준으로 해당 프로젝트가 어떤 타겟을 가질지 내부적으로 판단합니다.
  • 즉, Nx는 project.json 없이도 타겟이 존재하는 것처럼 동작할 수 있습니다.

targetDefaults

targetDefaults는 Nx에서 각 프로젝트의 타겟(build, test, lint 등)에 대해 기본적으로 적용할 설정값을 전역으로 정의하는 섹션입니다.

"targetDefaults": {
  "test": {
    "dependsOn": ["^build"]
  }
}

이렇게 설정하면, nx test my-app 명령을 실행할 때: 1. my-app이 의존하는 libs/utils, libs/db 등 Nx 내부 라이브러리들의 build가 먼저 실행됨 2. 그 후에 my-app의 test가 실행됨

이걸 통해 빌드가 되지 않은 의존성을 가진 채 테스트가 실행되는 문제를 방지할 수 있습니다.

Nx Cloud와 nxCloudId의 역할

Nx Cloud는 Nx에서 제공하는 고성능 분산 빌드 캐시 및 원격 실행 플랫폼입니다.
대규모 모노레포 환경에서 개발 속도와 CI/CD 효율성을 극대화하는 데 중요한 역할을 합니다.


Nx Cloud란?

  • Nx Cloud는 빌드, 테스트, 린트 등의 작업 결과를 원격 서버에 캐시하여
    동일한 작업이 다시 실행될 때 캐시된 결과를 재사용할 수 있도록 합니다.
  • 이를 통해 팀원 간, CI 서버 간 중복 작업을 제거하고, 작업 시간을 크게 줄일 수 있습니다.
  • Nx CLI와 완전히 통합되어 있어 설정만 해주면 자동으로 캐시 동기화가 이루어집니다.

nxCloudId란?

"nxCloudId": "6815cb530ad2c3276ebfb33e"

이 ID는 해당 워크스페이스가 어떤 Nx Cloud 저장소와 연결되어 있는지를 식별하는 데 사용됩니다

tsconfig.base.json

Nx 모노레포의 TypeScript 설정 구조 (tsconfig 계층 구조)

Nx 모노레포에서는 여러 앱과 라이브러리를 함께 관리하기 위해 TypeScript 설정을 계층적으로 분리하여 구성합니다.
이로 인해 중복 없이 일관된 개발 환경을 유지할 수 있습니다.

tsconfig.base.json ← 전역 TypeScript 설정 (공통) tsconfig.json ← 루트 프로젝트 참조 설정 (references) apps/xxx/tsconfig.json ← 각 앱/라이브러리의 개별 설정 (extends + references)

tsconfig.base.json전역 공통 설정

루트에 위치하며, 모든 프로젝트에서 공통으로 사용하는 TypeScript 설정을 담고 있습니다.

{
  "compilerOptions": {
    "composite": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "importHelpers": true,
    "isolatedModules": true,
    "lib": ["es2022"],
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2022",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@org/auth": ["libs/auth/src/index.ts"],
      "@org/db": ["libs/db/src/index.ts"],
      "@org/utils": ["libs/utils/src/index.ts"]
    },
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

주요 옵션 설명

  • composite: 프로젝트 참조를 가능하게 함

  • emitDeclarationOnly: .d.ts 만 출력하여 빌드 최적화

  • module, moduleResolution: nodejs 스타일 모듈 사용 (nodeNext는 ESM 호환)

    • module: "nodenext"
      Node.js의 ESM 환경과 호환되는 방식으로 모듈을 내보내기 위한 설정입니다.
      ES Modules(import, export)을 완전히 지원하며 .js / .mjs / .cjs 확장자 기반 로딩도 지원합니다.

    • moduleResolution: "nodenext"
      모듈을 찾을 때 Node.js의 ESM 규칙을 따릅니다.
      확장자 필수 명시, package.json"exports" 필드 해석 등도 포함됩니다.

  • paths : nx의 별칭 import 처리

  • experimentalDecorators, emitDecoratorMetadata: nestjs에서 데코레이터를 사용하기 위해 설정

    • 아직 ECMAScript 표준에 채틱되지 않아 설정

tsconfig.json

{
  "extends": "./tsconfig.base.json",
  "compileOnSave": false,
  "files": [],
  "references": [
    { "path": "./apps/nestjs-monorepo-nx-example" },
    { "path": "./libs/db" },
    { "path": "./libs/auth" },
    { "path": "./apps/user-web" },
    { "path": "./libs/utils" },
    ...
  ]
}

이 파일은 모든 프로젝트의 /apps, /libs의 경로를 연결(reference)하여 tsc –build 명령으로 전체 빌드가 가능하게끔 합니다.

모노레포 어플리케이션 예시와 설명

이 문서는 온라인 쇼핑몰 시스템을 Nx 모노레포로 구성하면서 설계한 프로젝트 구조, 의존성 방향, 각 앱 및 라이브러리의 역할에 대해 설명합니다. 모든 소스 코드는 apps/libs/로 분리됩니다. 코드는 여기에서 확인할 수 있습니다.

온라인 쇼핑몰 시스템
├── apps/
│   ├── user-web/              # 사용자용 웹 애플리케이션
│   ├── admin-dashboard/       # 관리자 대시보드 애플리케이션
│   ├── payment-service/       # 결제 마이크로서비스
│   └── inventory-service/     # 재고 관리 마이크로서비스
└── libs/
    ├── db/                    # 데이터베이스 접근 라이브러리 (Prisma 기반)
    ├── auth/                  # 인증/인가 라이브러리
    ├── utils/                 # 공통 유틸리티 라이브러리
    ├── payment/               # 결제 처리 라이브러리
    └── inventory/             # 재고 관리 라이브러리

의존성 설계 원칙

  • 애플리케이션 → 라이브러리 방향만 허용 (역방향 금지)
  • 하위 → 상위 비즈니스 로직 방향 금지 (utils, db는 절대 auth, payment에 의존하지 않음)
  • 순환 의존성 없음 (A → B → A 구조는 존재하지 않음)

각 앱/라이브러리의 역할

애플리케이션 (apps/)

이름설명
user-web고객이 상품 검색, 장바구니, 주문을 할 수 있는 웹 UI
admin-dashboard관리자가 제품, 주문, 사용자 등을 관리하는 웹 UI
payment-service결제 및 환불 처리 로직을 담당하는 백엔드 서비스
inventory-service재고 관리, 재고 이력 추적을 담당하는 백엔드 서비스

라이브러리 (libs/)

이름설명
utils날짜 포맷, 가격 계산 등 공통 유틸리티 (의존성 없음)
dbPrisma ORM 기반 DB 접근 모듈
authJWT 인증, 역할 기반 권한 부여 (db에 의존)
payment다양한 결제 수단 처리 로직 (신용카드, PayPal 등)
inventory재고 수량 추적 및 알림 로직 (db에 의존)

모노레포 구조의 장점

  • 책임 명확화: 각 라이브러리/앱은 역할이 분리되어 있고, 소유권이 분명함
  • 변경 영향 추적: Nx의 nx affected로 변경 영향 분석 가능
  • 코드 재사용: 공통 로직을 중복 없이 여러 앱에서 사용 가능
  • 독립 배포 가능: 각 앱은 필요한 라이브러리만 포함하여 개별 배포 가능
  • 확장성: 기능이 늘어나도 라이브러리를 추가하는 방식으로 손쉽게 확장 가능

Nx 빌드 및 실행 명령어 정리

Nx는 모노레포 환경에서 효율적인 개발을 돕기 위해 다양한 CLI 명령어를 제공합니다.
특히 빌드, 테스트, 린트, 실행(run), 영향 분석(affected) 관련 명령어는 일상적인 개발에서 가장 자주 사용됩니다.

기본 빌드 명령어

nx build [project]

  • 지정한 프로젝트를 빌드합니다.
  • 프로젝트가 라이브러리일 경우, 타입 선언 파일만 생성될 수도 있습니다.
  • 설정된 빌드 타겟(project.json 또는 plugin)에 따라 webpack, tsc, vite 등으로 빌드 수행.

nx serve [project]

  • 개발 서버를 실행합니다.
  • 웹 애플리케이션 프로젝트의 경우 webpack-dev-server 또는 vite dev가 실행됩니다.
  • NestJS 애플리케이션의 경우 내부적으로 ts-node-dev 또는 node dist/main.js 실행 가능.

테스트 및 린트 명령어

nx test [project]

  • Jest 또는 설정된 테스트 러너를 사용해 단위 테스트를 실행합니다.

nx lint [project]

  • ESLint를 사용해 해당 프로젝트의 린트를 수행합니다.

실행 명령어

nx run [project]:[target]

  • 지정한 타겟(build, test, lint 등)을 명시적으로 실행합니다.
  • 예: nx run user-web:build

이 명령어는 기본적으로 nx build user-web과 동일하지만,
custom target을 정의했을 경우 유용합니다. (예: nx run api:seed)

영향 분석 명령어 (affected)

Nx는 Git 기반 변경사항 분석을 통해 변경된 프로젝트와 그에 따라 영향받는 프로젝트만 선택적으로 빌드/테스트할 수 있습니다.

nx affected:graph --base=main

  • 현재 브랜치와 main 브랜치 간 변경된 파일을 기준으로 영향받는 프로젝트를 시각화합니다.

nx affected:build --base=main

  • 변경된 파일이 영향을 주는 프로젝트만 빌드합니다.
  • 전체 모노레포가 아닌, 필요한 부분만 빠르게 빌드 가능.

nx affected:test, nx affected:lint

  • 테스트나 린트 작업도 동일하게 변경 영향받는 프로젝트에만 적용됩니다.

병렬 실행 명령어

nx run-many --target=[target] --projects=[a,b,c] --parallel

  • 여러 프로젝트를 한꺼번에 실행합니다.
  • --parallel 플래그를 주면 의존성 그래프를 기반으로 병렬 실행합니다.
  • 예: nx run-many --target=build --projects=web,api --parallel

nx run-many --target=[target] --all

  • 워크스페이스의 모든 프로젝트에 대해 지정한 타겟을 실행합니다.
  • 전체 빌드나 전체 테스트가 필요할 때 유용합니다.

캐싱 관련

  • Nx는 기본적으로 계산 결과를 캐싱합니다.
  • 같은 명령을 같은 입력으로 재실행하면, 빌드나 테스트 없이 캐시된 결과를 재사용합니다.

캐시는 로컬에서 동작하며, Nx Cloud와 연결하면 CI/팀원 간 공유도 가능합니다.

기타 유용한 명령어

nx list

  • 현재 설치된 Nx 플러그인과 사용 가능한 스키마 목록을 확인할 수 있습니다.

nx graph

  • 전체 의존성 그래프를 브라우저에서 시각화하여 보여줍니다.

nx format:write / nx format:check

  • 워크스페이스 전체에 코드 포맷을 적용하거나 검사합니다.

요약 표

명령어설명
nx build [project]프로젝트 빌드
nx serve [project]개발 서버 실행
nx test [project]단위 테스트 실행
nx lint [project]린트 실행
nx run [project]:[target]명시적 타겟 실행
nx affected:graph변경 영향 프로젝트 시각화
nx affected:build영향받는 프로젝트만 빌드
nx run-many --target=build --projects=a,b여러 프로젝트 동시 빌드
nx run-many --target=test --all전체 테스트 실행
nx format:write코드 자동 포맷팅
nx graph의존성 그래프 보기

✅ Nx CLI는 모노레포의 복잡함을 효율적으로 제어하고,
불필요한 작업은 건너뛰고 필요한 작업만 정확히 수행할 수 있도록 설계된 강력한 도구입니다.

Nx Affected 기반 Docker 빌드 및 CI pipeline 자동화

NestJS 기반 모노레포를 Nx로 관리할 때, 변경된 앱만 효율적으로 빌드하고 Docker 이미지까지 생성하는 것은 CI/CD 파이프라인의 핵심이다.

이번 글에서는 Nx의 affected 기능을 활용해 변경된 앱만 자동으로 Docker build & push 하는 GitHub Actions 기반 CI 설정을 소개한다.

yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  actions: read
  contents: read

jobs:
  affected-docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci --legacy-peer-deps

      - uses: nrwl/nx-set-shas@v4

      - name: Get affected apps
        id: affected
        run: |
          echo "apps=$(npx nx show projects --affected --type=app --base=$NX_BASE --head=$NX_HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT          

      - name: Login to Docker (ECR or Registry)
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin ${{ secrets.DOCKER_REGISTRY }}          

      - name: Docker build & push affected apps
        run: |
          for app in ${{ steps.affected.outputs.apps }}; do
            echo "\n🐳 Building Docker image for: $app"
            docker build -f apps/$app/Dockerfile -t ${{ secrets.DOCKER_REGISTRY }}/$app:latest .
            docker push ${{ secrets.DOCKER_REGISTRY }}/$app:latest
          done          

이제 libs/utils처럼 공용 라이브러리 하나만 수정해도, 그 영향을 받는 앱만 Docker 이미지로 빌드되어 푸시된다. 이를 통해 낭비되는 빌드 시간 제거, 빠른 CI 피드백 루프 확보, 효율적인 배포 준비가 가능하다.