Skip to content
Published on

모노레포 전략 가이드 2025: Nx vs Turborepo vs Lerna — 대규모 코드베이스 관리의 모든 것

Authors

들어가며: 왜 모노레포인가

Google은 20억 줄의 코드를 하나의 저장소에서 관리합니다. Meta, Microsoft, Uber, Airbnb도 모노레포를 사용합니다. 모노레포는 더 이상 실험적 전략이 아니라, 대규모 소프트웨어 개발의 검증된 패턴입니다.

하지만 모노레포를 잘못 운영하면 CI가 30분 넘게 걸리고, 의존성 지옥에 빠지며, 코드 소유권이 불명확해집니다. 이 글에서는 모노레포의 올바른 도입 전략부터 Nx, Turborepo, Lerna의 심층 비교, CI/CD 최적화, 팀 운영 전략까지 모든 것을 다룹니다.


1. 모노레포 vs 폴리레포

1.1 정의

  • 모노레포(Monorepo): 여러 프로젝트/패키지를 단일 저장소에서 관리
  • 폴리레포(Polyrepo): 각 프로젝트를 개별 저장소에서 관리 (=멀티레포)

1.2 비교표

항목모노레포폴리레포
코드 공유즉시 (같은 저장소)npm/레지스트리 통해
의존성 관리통합 관리각 저장소별 독립
원자적 변경가능 (하나의 PR)불가 (여러 PR 필요)
CI 복잡도높음 (최적화 필요)낮음 (각 저장소 독립)
코드 가시성전체 코드 검색 가능각 저장소 별도 탐색
릴리스 관리복잡 (Changesets 등)단순 (개별 버전)
권한 관리CODEOWNERS 필요저장소별 분리
초기 클론느릴 수 있음빠름
리팩토링전체 코드에 걸쳐 가능저장소 경계에서 어려움

1.3 언제 모노레포를 선택해야 하는가

모노레포가 적합한 경우:

  • 패키지 간 긴밀한 의존성이 있는 경우
  • 공유 라이브러리가 많은 경우
  • 원자적(atomic) 변경이 필요한 경우
  • 코드 재사용을 극대화하고 싶은 경우
  • 팀 간 코드 가시성이 중요한 경우

폴리레포가 적합한 경우:

  • 완전히 독립된 프로젝트들
  • 팀 간 기술 스택이 매우 다른 경우
  • 엄격한 접근 제어가 필요한 경우
  • 오픈소스와 비공개 코드를 분리해야 하는 경우

2. 대규모 기업의 모노레포 전략

2.1 Google (Piper)

  • 규모: 20억 줄 이상, 86TB
  • 도구: Piper(자체 VCS) + Blaze/Bazel(빌드)
  • 핵심: 모든 코드를 하나의 저장소에서 관리, 트렁크 기반 개발
  • 교훈: 적절한 도구 없이는 불가능. 커스텀 VCS와 빌드 시스템이 필수

2.2 Meta (Buck2)

  • 도구: Mercurial + Buck2(빌드 시스템)
  • 핵심: Virtual filesystem으로 필요한 파일만 로드
  • 교훈: 대규모에서는 파일 시스템 레벨의 최적화가 필요

2.3 Microsoft (1JS)

  • 도구: Git + Rush(빌드 오케스트레이션)
  • 핵심: JavaScript/TypeScript 프로젝트 통합 관리
  • 교훈: 점진적 마이그레이션이 현실적인 전략

2.4 Uber

  • 도구: Go 모노레포 + Buck(빌드)
  • 핵심: 5000개 이상의 마이크로서비스를 단일 저장소에서
  • 교훈: 마이크로서비스와 모노레포는 공존 가능

3. 도구 비교: Nx vs Turborepo vs Lerna vs Rush

3.1 기능 비교표

기능NxTurborepoLernaRush
태스크 파이프라인OOO (v7+)O
로컬 캐싱OOXO
원격 캐싱O (Nx Cloud)O (Vercel)XO (자체)
Affected 감지O (프로젝트 그래프)O (파일 해시)O (v7+)O
코드 생성기O (generators)XXX
프로젝트 그래프 시각화OXXX
프레임워크 플러그인O (React, Angular 등)XXX
분산 실행O (Nx Agents)XXO
패키지 매니저npm, yarn, pnpmnpm, yarn, pnpmnpm, yarn, pnpmpnpm
학습 곡선높음낮음낮음중간
오픈소스OOOO

3.2 도구 선택 가이드

프로젝트 시작?
├── 소규모 (패키지 10개 미만)
│   ├── 빠른 시작 원함 → Turborepo
│   └── 코드 생성 필요 → Nx
├── 중규모 (패키지 10-50)
│   ├── Vercel/Next.js 생태계 → Turborepo
│   ├── 풍부한 플러그인 필요 → Nx
│   └── 기존 Lerna 사용 중 → Lerna v7
└── 대규모 (패키지 50개 이상)
    ├── 분산 실행 필요 → Nx
    └── Microsoft 스타일 → Rush

4. pnpm Workspace 설정

4.1 기본 구조

my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
├── apps/
│   ├── web/
│   │   └── package.json
│   └── api/
│       └── package.json
├── packages/
│   ├── ui/
│   │   └── package.json
│   ├── utils/
│   │   └── package.json
│   └── config/
│       └── package.json
└── tooling/
    ├── eslint/
    │   └── package.json
    └── typescript/
        └── package.json

4.2 pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"

4.3 루트 package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@9.0.0"
}

4.4 패키지 간 참조

{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

workspace:*는 로컬 패키지를 직접 참조합니다. npm에 배포할 때 pnpm이 실제 버전으로 교체합니다.

4.5 .npmrc 설정

# 호이스팅 설정
shamefully-hoist=false
strict-peer-dependencies=false

# 워크스페이스 설정
link-workspace-packages=true
prefer-workspace-packages=true

5. Nx 심층 분석

5.1 Nx 초기 설정

# 새 Nx 워크스페이스 생성
npx create-nx-workspace@latest my-monorepo --preset=ts

# 기존 모노레포에 Nx 추가
npx nx@latest init

5.2 nx.json 설정

{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/tsconfig.spec.json"
    ],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "nxCloudAccessToken": "your-token-here"
}

5.3 프로젝트 그래프

# 프로젝트 의존성 그래프 시각화
npx nx graph

# 특정 프로젝트의 의존 관계 확인
npx nx graph --focus=my-app

# 영향받는 프로젝트 확인
npx nx affected:graph
프로젝트 그래프 예시:

    web-app ───▶ ui-lib ───▶ utils
       │            │
       ▼            ▼
    api-app ───▶ shared-types

5.4 Affected 명령 (변경 감지)

# 변경된 코드에 영향받는 프로젝트만 빌드
npx nx affected -t build

# 영향받는 프로젝트만 테스트
npx nx affected -t test

# base 브랜치 지정
npx nx affected -t build --base=main --head=HEAD

Affected 동작 원리:

  1. Git diff로 변경된 파일 감지
  2. 프로젝트 그래프에서 해당 파일이 속한 프로젝트 찾기
  3. 의존성 그래프를 따라 영향받는 모든 프로젝트 식별
  4. 해당 프로젝트들만 실행

5.5 Generators (코드 생성기)

# React 컴포넌트 생성
npx nx generate @nx/react:component Button --project=ui

# 라이브러리 생성
npx nx generate @nx/js:library shared-utils

# 커스텀 Generator 생성
npx nx generate @nx/plugin:generator my-generator --project=tools

5.6 Nx Cloud (원격 캐싱 + 분산 실행)

# Nx Cloud 연결
npx nx connect

# 분산 실행 설정 (CI에서)
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
      - run: pnpm install --frozen-lockfile
      - uses: nrwl/nx-set-shas@v4
      - run: npx nx affected -t lint test build

6. Turborepo 심층 분석

6.1 Turborepo 초기 설정

# 새 Turborepo 프로젝트 생성
npx create-turbo@latest

# 기존 모노레포에 Turborepo 추가
pnpm add -D turbo -w

6.2 turbo.json 설정

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "globalEnv": ["NODE_ENV"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["DATABASE_URL"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["$TURBO_DEFAULT$"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  }
}

6.3 Task Pipeline 동작 원리

turbo run build 실행 시:

1. 의존성 그래프 분석
   utils (의존 없음)ui (utils에 의존)web (ui, utils에 의존)

2. 병렬 실행
   [utils: build] ──완료──▶ [ui: build] ──완료──▶ [web: build]
                            [api: build] ──────────┘
                              (utils에만 의존)

3. 캐싱
   - 입력 파일의 해시 계산
   - 이전 빌드와 해시 동일하면 캐시에서 복원
   - 빌드 시간: 5분 → 0.1 (캐시 히트)

6.4 캐싱 메커니즘

# 로컬 캐시 위치
ls node_modules/.cache/turbo/

# 캐시 상태 확인
turbo run build --dry-run

# 캐시 무효화
turbo run build --force

# 원격 캐시 활성화
npx turbo login
npx turbo link

6.5 필터링과 스코핑

# 특정 패키지만 빌드
turbo run build --filter=@myorg/web

# 변경된 패키지만 빌드
turbo run build --filter=...[HEAD^1]

# 특정 패키지와 그 의존성만 빌드
turbo run build --filter=@myorg/web...

# 특정 디렉토리의 패키지만
turbo run build --filter=./apps/*

6.6 원격 캐싱 설정

# Vercel Remote Cache (공식)
npx turbo login
npx turbo link

# 셀프호스팅 (ducktape/turborepo-remote-cache)
# turbo.json에 추가:
{
  "remoteCache": {
    "signature": true
  }
}

환경 변수 설정:

TURBO_TOKEN=your-token
TURBO_TEAM=your-team
TURBO_API=https://your-cache-server.com

7. 공유 라이브러리 전략

7.1 패키지 분류

packages/
├── ui/              # UI 컴포넌트 (Button, Modal, Form)
├── utils/           # 유틸리티 (날짜, 문자열, 검증)
├── types/           # 공유 TypeScript 타입
├── config/          # 공유 설정 (ESLint, TypeScript, Prettier)
├── hooks/           # 공유 React hooks
├── api-client/      # API 클라이언트 (타입 안전 fetch)
└── constants/       # 상수 (에러 코드, 라우트 경로)

7.2 Internal Package (내부 패키지) 패턴

빌드 없이 TypeScript 소스를 직접 참조하는 패턴:

{
  "name": "@myorg/ui",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts",
    "./*": "./src/*.ts"
  }
}

소비하는 앱의 tsconfig.json에서 path alias 설정:

{
  "compilerOptions": {
    "paths": {
      "@myorg/ui": ["../../packages/ui/src"],
      "@myorg/ui/*": ["../../packages/ui/src/*"]
    }
  }
}

7.3 빌드된 패키지 패턴

npm에 배포할 패키지는 별도 빌드 필요:

{
  "name": "@myorg/utils",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts"
  }
}

8. 버전 관리와 Changesets

8.1 Changesets 소개

Changesets는 모노레포에서 버전 관리와 체인지로그 생성을 자동화하는 도구입니다.

# 설치
pnpm add -D @changesets/cli -w

# 초기화
pnpm changeset init

8.2 워크플로우

# 1. 변경사항 설명 추가
pnpm changeset
# ? 어떤 패키지가 변경되었나요? → @myorg/ui, @myorg/utils
# ? 변경 유형은? → minor (새 기능)
# ? 변경 설명은? → Added new Button variant

# 2. .changeset/ 디렉토리에 파일 생성됨
cat .changeset/brave-dogs-run.md
# ---
# "@myorg/ui": minor
# "@myorg/utils": patch
# ---
#
# Added new Button variant

# 3. 버전 업데이트 (CI에서 실행)
pnpm changeset version

# 4. 배포
pnpm changeset publish

8.3 설정 (.changeset/config.json)

{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [["@myorg/ui", "@myorg/hooks"]],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@myorg/web", "@myorg/api"]
}
  • linked: 함께 버전이 올라가는 패키지 그룹
  • fixed: 항상 같은 버전을 유지하는 패키지 그룹
  • ignore: 배포 대상에서 제외할 패키지 (앱 등)

8.4 Independent vs Fixed 버전 관리

전략설명예시
Independent각 패키지 독립 버전ui@2.3.0, utils@1.5.0
Fixed모든 패키지 동일 버전ui@3.0.0, utils@3.0.0
Linked관련 패키지 연동 버전ui@2.3.0 바뀌면 hooks@2.3.0도

9. CI/CD 최적화

9.1 Affected 빌드 전략

변경된 파일에 영향받는 프로젝트만 빌드/테스트합니다.

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
    branches: [main]

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

      - uses: pnpm/action-setup@v2
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "pnpm"

      - run: pnpm install --frozen-lockfile

      # Turborepo: 변경된 패키지만 빌드
      - run: turbo run build test lint --filter=...[origin/main]

      # 또는 Nx: 영향받는 프로젝트만
      # - run: npx nx affected -t build test lint

9.2 원격 캐싱으로 CI 시간 단축

캐시 없이:
  lint (2) + test (5) + build (8) = 15
원격 캐시 히트:
  lint (0.1) + test (0.2) + build (0.3) = 0.6
실제 변경된 패키지만:
  lint (20) + test (1) + build (2) = 320

9.3 병렬 실행 전략

# 매트릭스 전략으로 병렬 실행
jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.filter.outputs.packages }}
    steps:
      - uses: actions/checkout@v4
      - id: filter
        run: echo "packages=$(turbo run build --filter=...[origin/main] --dry-run=json | jq -c '.packages')" >> $GITHUB_OUTPUT

  build:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: ${{ fromJson(needs.detect.outputs.packages) }}
    steps:
      - uses: actions/checkout@v4
      - run: turbo run build --filter=${{ matrix.package }}

9.4 Docker 빌드 최적화

# 모노레포에서의 Docker 빌드
FROM node:20-slim AS base
RUN corepack enable

FROM base AS pruned
WORKDIR /app
COPY . .
# turbo prune: 특정 앱과 의존성만 추출
RUN npx turbo prune @myorg/api --docker

FROM base AS installer
WORKDIR /app
# 의존성만 먼저 설치 (캐시 레이어)
COPY --from=pruned /app/out/json/ .
COPY --from=pruned /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile

# 소스 복사 및 빌드
COPY --from=pruned /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/api

FROM base AS runner
WORKDIR /app
COPY --from=installer /app/apps/api/dist ./dist
COPY --from=installer /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]

10. CODEOWNERS와 팀 경계

10.1 CODEOWNERS 파일

# .github/CODEOWNERS

# 전역 기본 소유자
* @org/platform-team

# 앱별 소유자
/apps/web/          @org/frontend-team
/apps/api/          @org/backend-team
/apps/mobile/       @org/mobile-team

# 패키지별 소유자
/packages/ui/       @org/design-system-team
/packages/utils/    @org/platform-team
/packages/auth/     @org/security-team

# 설정 파일
/tooling/           @org/dx-team
/.github/           @org/dx-team
/turbo.json         @org/dx-team

10.2 팀 경계 설정 (Nx Module Boundaries)

// .eslintrc.json
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "scope:web",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:web"]
          },
          {
            "sourceTag": "scope:api",
            "onlyDependOnLibsWithTags": ["scope:shared", "scope:api"]
          },
          {
            "sourceTag": "type:app",
            "onlyDependOnLibsWithTags": ["type:lib", "type:util"]
          },
          {
            "sourceTag": "type:lib",
            "onlyDependOnLibsWithTags": ["type:lib", "type:util"]
          }
        ]
      }
    ]
  }
}

10.3 PR 리뷰 자동 할당

# .github/workflows/auto-assign.yml
name: Auto Assign Reviewers
on:
  pull_request:
    types: [opened]

jobs:
  assign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Auto assign based on changed files
        uses: kentaro-m/auto-assign-action@v2
        with:
          configuration-path: .github/auto-assign.yml

11. 폴리레포에서 모노레포로 마이그레이션

11.1 단계별 마이그레이션

1단계: 준비

# 새 모노레포 저장소 생성
mkdir my-monorepo && cd my-monorepo
git init
pnpm init

2단계: Git 히스토리를 보존하며 이동

# 기존 저장소를 서브디렉토리로 이동 (히스토리 보존)
git remote add -f web-repo https://github.com/org/web-app.git
git merge web-repo/main --allow-unrelated-histories

# git-filter-repo로 디렉토리 구조 변경
git filter-repo --to-subdirectory-filter apps/web

3단계: 워크스페이스 설정

# pnpm workspace 설정
cat > pnpm-workspace.yaml << 'EOF'
packages:
  - "apps/*"
  - "packages/*"
EOF

# 루트 package.json 설정
# (위 4.3절 참조)

4단계: 공유 패키지 추출

# 중복 코드를 공유 패키지로 추출
mkdir -p packages/shared-utils/src
# 공통 유틸 이동 및 패키지 설정

5단계: CI/CD 업데이트

# Turborepo 또는 Nx 설정
pnpm add -D turbo -w
# turbo.json 설정 (위 6.2절 참조)

11.2 점진적 마이그레이션 전략

한 번에 모든 저장소를 옮기지 마세요. 추천 순서:

  1. 공유 라이브러리 먼저 이동
  2. 가장 많이 의존하는 앱 이동
  3. 나머지 앱 순차적 이동
  4. 각 단계에서 CI/CD 검증

12. 일반적인 함정과 해결책

12.1 느린 CI

문제: 모든 패키지를 매번 빌드/테스트하여 CI가 30분 이상 소요

해결:

  • Affected 명령 사용 (변경된 것만 빌드)
  • 원격 캐싱 활성화
  • 병렬 실행 설정
# Before: 모든 패키지 빌드 (15분)
pnpm -r run build

# After: 변경된 패키지만 (2분)
turbo run build --filter=...[origin/main]

12.2 의존성 지옥

문제: 패키지 A가 lodash@4를 쓰고 패키지 B가 lodash@3을 쓸 때

해결:

  • pnpm의 strict mode 사용 (phantom dependency 방지)
  • 공유 의존성은 루트에서 관리
  • syncpack으로 버전 동기화
# syncpack으로 의존성 버전 확인
npx syncpack list-mismatches
npx syncpack fix-mismatches

12.3 코드 소유권 불명확

문제: 누가 어떤 코드를 책임지는지 불명확

해결:

  • CODEOWNERS 파일 설정 (10.1절 참조)
  • Nx module boundaries로 의존성 제한
  • 팀별 패키지 네임스페이스

12.4 초기 클론 시간

문제: 대규모 모노레포의 git clone이 10분 이상 소요

해결:

# Shallow clone
git clone --depth 1 https://github.com/org/monorepo.git

# Partial clone (blob 없이)
git clone --filter=blob:none https://github.com/org/monorepo.git

# Sparse checkout (특정 디렉토리만)
git clone --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set apps/web packages/ui

12.5 빌드 순서 문제

문제: 패키지 A가 패키지 B에 의존하는데 B가 아직 빌드되지 않음

해결: Task pipeline의 dependsOn 설정

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    }
  }
}

^build는 현재 패키지의 의존성들의 build를 먼저 실행하라는 의미입니다.


13. 면접 질문 모음 (10문제)

Q1. 모노레포와 폴리레포의 차이점과 각각의 장단점을 설명하라.

모범 답변: 모노레포는 여러 프로젝트를 단일 저장소에서 관리하여 코드 공유가 용이하고 원자적 변경이 가능하지만 CI 최적화가 필요합니다. 폴리레포는 각 프로젝트를 독립 저장소로 관리하여 독립성이 높지만 코드 공유가 어렵고 크로스 저장소 변경이 복잡합니다.

Q2. Nx와 Turborepo의 핵심 차이점은?

모범 답변: Nx는 프로젝트 그래프 시각화, 코드 생성기, 프레임워크 플러그인, 분산 실행 등 풍부한 기능을 제공하지만 학습 곡선이 높습니다. Turborepo는 캐싱과 태스크 파이프라인에 집중하여 설정이 간단하지만 코드 생성이나 시각화 기능은 없습니다.

Q3. Affected 명령의 동작 원리를 설명하라.

모범 답변: Git diff로 변경된 파일을 감지하고, 프로젝트 그래프에서 해당 파일이 속한 프로젝트와 그에 의존하는 모든 프로젝트를 찾아 해당 프로젝트들만 빌드/테스트합니다. 이를 통해 CI 시간을 90% 이상 줄일 수 있습니다.

Q4. 원격 캐싱이 CI 성능을 개선하는 원리는?

모범 답변: 빌드의 입력(소스 코드, 설정 등)을 해싱하여 동일한 입력이면 이전 빌드 결과를 클라우드 캐시에서 복원합니다. 동일한 코드를 여러 개발자가 빌드하거나 CI에서 반복 빌드할 때 빌드를 건너뛸 수 있습니다.

Q5. pnpm workspace의 workspace:* 프로토콜은 무엇인가?

모범 답변: workspace:*는 로컬 워크스페이스의 패키지를 직접 참조하는 프로토콜입니다. 심볼릭 링크로 연결되어 빌드 없이 소스 변경이 즉시 반영됩니다. npm에 배포할 때 pnpm이 자동으로 실제 버전 번호로 교체합니다.

Q6. Changesets의 워크플로우를 설명하라.

모범 답변: 개발자가 pnpm changeset으로 변경사항을 기술하면 .changeset/ 디렉토리에 마크다운 파일이 생성됩니다. PR 머지 후 CI에서 changeset version으로 패키지 버전을 업데이트하고, changeset publish로 npm에 배포합니다.

Q7. CODEOWNERS가 모노레포에서 중요한 이유는?

모범 답변: 모노레포에서는 여러 팀의 코드가 한 저장소에 있어 코드 소유권이 불명확해질 수 있습니다. CODEOWNERS 파일로 디렉토리별 담당 팀을 지정하면, PR에 자동으로 리뷰어가 할당되어 코드 품질과 책임 소재를 보장합니다.

Q8. 모노레포에서 Docker 빌드를 최적화하는 방법은?

모범 답변: Turborepo의 turbo prune으로 특정 앱과 그 의존성만 추출한 뒤 Docker에서 빌드합니다. 멀티스테이지 빌드에서 의존성 설치와 소스 빌드를 분리하여 Docker 레이어 캐싱을 활용합니다.

Q9. 폴리레포에서 모노레포로 마이그레이션할 때 주의점은?

모범 답변: Git 히스토리 보존이 중요합니다. git-filter-repo로 서브디렉토리 이동 시 히스토리를 유지할 수 있습니다. 한 번에 모든 저장소를 이전하지 말고 공유 라이브러리부터 점진적으로 이동하며, 각 단계에서 CI/CD를 검증해야 합니다.

Q10. 모노레포에서 Nx module boundaries의 역할은?

모범 답변: ESLint 규칙으로 패키지 간 의존성을 제한합니다. 프로젝트에 태그를 지정하고, 특정 태그의 프로젝트만 참조할 수 있게 규칙을 설정합니다. 예를 들어 frontend 앱이 backend 전용 패키지를 참조하는 것을 방지합니다.


14. 실전 퀴즈 (5문제)

Q1. turbo.json에서 "dependsOn": ["^build"]의 의미는?

정답: ^ 접두사는 현재 패키지의 의존성(dependencies) 들의 build 태스크를 먼저 실행하라는 의미입니다. 예를 들어 패키지 A가 패키지 B에 의존하면, A의 build 전에 B의 build가 먼저 완료됩니다. ^ 없이 ["build"]만 쓰면 같은 패키지 내의 다른 태스크 의존성을 의미합니다.

Q2. pnpm의 shamefully-hoist=false가 모노레포에서 중요한 이유는?

정답: pnpm은 기본적으로 패키지를 격리된 node_modules 구조에 설치하여 phantom dependency(선언하지 않은 의존성 사용)를 방지합니다. shamefully-hoist=false는 이 엄격한 격리를 유지합니다. 호이스팅을 활성화하면 선언하지 않은 패키지에 접근할 수 있어, 나중에 배포 시 문제가 발생할 수 있습니다.

Q3. Changesets에서 linked와 fixed의 차이는?

정답: linked는 그룹 내 패키지 중 하나의 버전이 올라가면 나머지도 같은 수준으로 올라갑니다 (예: 하나가 minor 올라가면 나머지도 minor). fixed는 그룹 내 모든 패키지가 항상 동일한 버전 번호를 유지합니다 (예: 모두 3.0.0에서 3.1.0으로).

Q4. git clone --filter=blob:none이 모노레포에 유용한 이유는?

정답: Partial clone으로 blob(파일 내용)을 초기에 다운로드하지 않고, 필요할 때만 가져옵니다. 대규모 모노레포에서 초기 클론 시간을 크게 줄일 수 있습니다. Git이 체크아웃하는 파일만 blob을 가져오므로, sparse checkout과 함께 사용하면 필요한 파일만 최소한으로 다운로드합니다.

Q5. 원격 캐싱에서 캐시 키가 결정되는 방식은?

정답: 소스 파일, 설정 파일, 환경 변수, 의존성의 빌드 결과 등 태스크의 모든 입력을 해싱하여 캐시 키를 생성합니다. 동일한 입력이면 동일한 해시가 나오므로, 이전에 빌드한 결과를 캐시에서 복원할 수 있습니다. Turborepo는 turbo.jsoninputsenv 설정으로 캐시 키에 포함할 요소를 정의합니다.


15. 참고 자료

  1. Nx Documentation — Nx 공식 문서
  2. Turborepo Documentation — Turborepo 공식 문서
  3. Changesets Documentation — Changesets 공식 문서
  4. pnpm Workspace — pnpm 워크스페이스 공식 문서
  5. Google Monorepo Paper — Why Google Stores Billions of Lines of Code in a Single Repository
  6. Lerna Documentation — Lerna 공식 문서
  7. Rush Documentation — Rush 공식 문서 (Microsoft)
  8. Monorepo Explained — 모노레포 도구 비교 사이트
  9. Turborepo Caching — 캐싱 메커니즘 상세
  10. Nx Affected — Affected 명령 동작 원리
  11. Git Sparse Checkout — 대규모 저장소 최적화
  12. CODEOWNERS Syntax — GitHub CODEOWNERS 문법