NestJS Monorepo 구조 설계

NestJS Monorepo란?

프로젝트가 커지면 여러 NestJS 앱(API 서버, Worker, Admin)과 공통 라이브러리를 관리해야 한다. 각각 별도 저장소로 운영하면 코드 중복, 버전 불일치, 배포 복잡도가 기하급수적으로 늘어난다. NestJS Monorepo는 하나의 저장소에서 여러 애플리케이션과 공유 라이브러리를 함께 관리하는 공식 지원 구조다.

Monorepo 초기화

# 기존 프로젝트를 Monorepo로 전환
nest generate app worker

# 또는 처음부터 Monorepo로 시작
nest new my-project
cd my-project
nest generate app admin
nest generate app worker
nest generate library common

nest generate app을 실행하면 자동으로 Monorepo 모드로 전환되고 nest-cli.json에 프로젝트가 등록된다.

디렉토리 구조

my-project/
├── apps/
│   ├── api/                    # 메인 API 서버
│   │   ├── src/
│   │   │   ├── app.module.ts
│   │   │   └── main.ts
│   │   ├── test/
│   │   └── tsconfig.app.json
│   ├── admin/                  # 관리자 API
│   │   ├── src/
│   │   │   ├── app.module.ts
│   │   │   └── main.ts
│   │   └── tsconfig.app.json
│   └── worker/                 # 백그라운드 워커
│       ├── src/
│       │   ├── app.module.ts
│       │   └── main.ts
│       └── tsconfig.app.json
├── libs/
│   ├── common/                 # 공통 유틸·DTO·인터페이스
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── dto/
│   │   │   ├── entities/
│   │   │   └── interfaces/
│   │   └── tsconfig.lib.json
│   └── database/               # DB 모듈·엔티티·마이그레이션
│       ├── src/
│       │   ├── index.ts
│       │   ├── database.module.ts
│       │   └── entities/
│       └── tsconfig.lib.json
├── nest-cli.json
├── tsconfig.json
└── package.json

nest-cli.json 설정

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/api/src",
  "monorepo": true,
  "root": "apps/api",
  "compilerOptions": {
    "webpack": true,
    "tsConfigPath": "apps/api/tsconfig.app.json"
  },
  "projects": {
    "api": {
      "type": "application",
      "root": "apps/api",
      "entryFile": "main",
      "sourceRoot": "apps/api/src",
      "compilerOptions": {
        "tsConfigPath": "apps/api/tsconfig.app.json"
      }
    },
    "admin": {
      "type": "application",
      "root": "apps/admin",
      "entryFile": "main",
      "sourceRoot": "apps/admin/src",
      "compilerOptions": {
        "tsConfigPath": "apps/admin/tsconfig.app.json"
      }
    },
    "worker": {
      "type": "application",
      "root": "apps/worker",
      "entryFile": "main",
      "sourceRoot": "apps/worker/src",
      "compilerOptions": {
        "tsConfigPath": "apps/worker/tsconfig.app.json"
      }
    },
    "common": {
      "type": "library",
      "root": "libs/common",
      "entryFile": "index",
      "sourceRoot": "libs/common/src",
      "compilerOptions": {
        "tsConfigPath": "libs/common/tsconfig.lib.json"
      }
    },
    "database": {
      "type": "library",
      "root": "libs/database",
      "entryFile": "index",
      "sourceRoot": "libs/database/src",
      "compilerOptions": {
        "tsConfigPath": "libs/database/tsconfig.lib.json"
      }
    }
  }
}

라이브러리(libs) 설계

라이브러리는 여러 앱에서 공유하는 코드를 모듈로 분리한 것이다. TypeScript path alias로 임포트한다.

// tsconfig.json (루트)
{
  "compilerOptions": {
    "paths": {
      "@app/common": ["libs/common/src"],
      "@app/common/*": ["libs/common/src/*"],
      "@app/database": ["libs/database/src"],
      "@app/database/*": ["libs/database/src/*"]
    }
  }
}
// libs/common/src/index.ts — 라이브러리 진입점
export * from './dto/pagination.dto';
export * from './dto/api-response.dto';
export * from './interfaces/base-service.interface';
export * from './decorators/current-user.decorator';
export * from './filters/all-exceptions.filter';
export * from './interceptors/logging.interceptor';
export * from './guards/api-key.guard';
// libs/database/src/database.module.ts
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useFactory: (config: ConfigService) => ({
        type: 'mysql',
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASS'),
        database: config.get('DB_NAME'),
        entities: [User, Order, Product],
        synchronize: false,
      }),
      inject: [ConfigService],
    }),
  ],
  exports: [TypeOrmModule],
})
export class DatabaseModule {}

앱에서 라이브러리 사용

// apps/api/src/app.module.ts
import { DatabaseModule } from '@app/database';
import { LoggingInterceptor, AllExceptionsFilter } from '@app/common';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    DatabaseModule,      // 공유 DB 모듈
    UsersModule,
    OrdersModule,
  ],
  providers: [
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_FILTER, useClass: AllExceptionsFilter },
  ],
})
export class AppModule {}

// apps/worker/src/app.module.ts
import { DatabaseModule } from '@app/database';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    DatabaseModule,      // 같은 DB 모듈 재사용
    BullModule.forRoot({ ... }),
    JobsModule,
  ],
})
export class WorkerModule {}

엔티티, DTO, 유틸리티를 한 곳에서 관리하므로 API와 Worker 간 타입 불일치가 원천적으로 방지된다.

빌드와 실행

# 특정 앱 개발 모드
nest start api --watch
nest start worker --watch
nest start admin --watch

# 특정 앱 빌드
nest build api
nest build worker

# 모든 앱 빌드 (스크립트 정의)
# package.json
{
  "scripts": {
    "start:api": "nest start api --watch",
    "start:worker": "nest start worker --watch",
    "start:admin": "nest start admin --watch",
    "build:api": "nest build api",
    "build:worker": "nest build worker",
    "build:all": "nest build api && nest build worker && nest build admin",
    "test:api": "jest --config apps/api/test/jest.config.ts",
    "test:libs": "jest --config libs/common/test/jest.config.ts"
  }
}

Docker 빌드 전략

Monorepo에서 각 앱을 별도 Docker 이미지로 빌드하되, 공유 라이브러리를 포함해야 한다.

# Dockerfile.api
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build:api

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist/apps/api ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
CMD ["node", "dist/main.js"]
# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.api
    ports: ["3000:3000"]
    env_file: .env

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    env_file: .env

Docker Multi-Stage Build를 활용하면 최종 이미지에 소스 코드 없이 빌드 결과물만 포함시킬 수 있다.

환경별 설정 분리

my-project/
├── .env                    # 공통
├── .env.api                # API 전용
├── .env.worker             # Worker 전용
├── apps/
│   ├── api/src/main.ts
│   └── worker/src/main.ts
// apps/api/src/app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.api', '.env'],  // API 전용 → 공통 순서
    }),
  ],
})

라이브러리 설계 원칙

라이브러리 포함 내용 의존성
@app/common DTO, 인터페이스, 데코레이터, 유틸 없음 (순수)
@app/database 엔티티, Repository, 마이그레이션 TypeORM
@app/auth 인증 모듈, Guard, JWT 전략 Passport, JWT
@app/messaging 이벤트 발행/구독, 큐 설정 BullMQ, Redis

핵심 원칙: 라이브러리 간 순환 의존을 피하자. common은 다른 라이브러리에 의존하지 않고, databasecommon만 의존하는 단방향 그래프를 유지한다.

정리

NestJS Monorepo는 코드 공유, 타입 일관성, 통합 테스트를 하나의 저장소에서 해결한다. libs/에 공통 모듈을 분리하고, apps/에 각 서비스를 배치하면 마이크로서비스의 독립성과 모노리스의 개발 편의성을 동시에 얻을 수 있다. 프로젝트가 서비스 2개를 넘어가면 Monorepo 전환을 진지하게 고려하자.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux