Split View: 모노레포 전략 가이드 2025: Nx vs Turborepo vs Lerna — 대규모 코드베이스 관리의 모든 것
모노레포 전략 가이드 2025: Nx vs Turborepo vs Lerna — 대규모 코드베이스 관리의 모든 것
- 들어가며: 왜 모노레포인가
- 1. 모노레포 vs 폴리레포
- 2. 대규모 기업의 모노레포 전략
- 3. 도구 비교: Nx vs Turborepo vs Lerna vs Rush
- 4. pnpm Workspace 설정
- 5. Nx 심층 분석
- 6. Turborepo 심층 분석
- 7. 공유 라이브러리 전략
- 8. 버전 관리와 Changesets
- 9. CI/CD 최적화
- 10. CODEOWNERS와 팀 경계
- 11. 폴리레포에서 모노레포로 마이그레이션
- 12. 일반적인 함정과 해결책
- 13. 면접 질문 모음 (10문제)
- Q1. 모노레포와 폴리레포의 차이점과 각각의 장단점을 설명하라.
- Q2. Nx와 Turborepo의 핵심 차이점은?
- Q3. Affected 명령의 동작 원리를 설명하라.
- Q4. 원격 캐싱이 CI 성능을 개선하는 원리는?
- Q5. pnpm workspace의 workspace:* 프로토콜은 무엇인가?
- Q6. Changesets의 워크플로우를 설명하라.
- Q7. CODEOWNERS가 모노레포에서 중요한 이유는?
- Q8. 모노레포에서 Docker 빌드를 최적화하는 방법은?
- Q9. 폴리레포에서 모노레포로 마이그레이션할 때 주의점은?
- Q10. 모노레포에서 Nx module boundaries의 역할은?
- 14. 실전 퀴즈 (5문제)
- 15. 참고 자료
들어가며: 왜 모노레포인가
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 기능 비교표
| 기능 | Nx | Turborepo | Lerna | Rush |
|---|---|---|---|---|
| 태스크 파이프라인 | O | O | O (v7+) | O |
| 로컬 캐싱 | O | O | X | O |
| 원격 캐싱 | O (Nx Cloud) | O (Vercel) | X | O (자체) |
| Affected 감지 | O (프로젝트 그래프) | O (파일 해시) | O (v7+) | O |
| 코드 생성기 | O (generators) | X | X | X |
| 프로젝트 그래프 시각화 | O | X | X | X |
| 프레임워크 플러그인 | O (React, Angular 등) | X | X | X |
| 분산 실행 | O (Nx Agents) | X | X | O |
| 패키지 매니저 | npm, yarn, pnpm | npm, yarn, pnpm | npm, yarn, pnpm | pnpm |
| 학습 곡선 | 높음 | 낮음 | 낮음 | 중간 |
| 오픈소스 | O | O | O | O |
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 동작 원리:
- Git diff로 변경된 파일 감지
- 프로젝트 그래프에서 해당 파일이 속한 프로젝트 찾기
- 의존성 그래프를 따라 영향받는 모든 프로젝트 식별
- 해당 프로젝트들만 실행
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분) = 3분 20초
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 /app/out/json/ .
COPY /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
# 소스 복사 및 빌드
COPY /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/api
FROM base AS runner
WORKDIR /app
COPY /app/apps/api/dist ./dist
COPY /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 점진적 마이그레이션 전략
한 번에 모든 저장소를 옮기지 마세요. 추천 순서:
- 공유 라이브러리 먼저 이동
- 가장 많이 의존하는 앱 이동
- 나머지 앱 순차적 이동
- 각 단계에서 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.json의 inputs와 env 설정으로 캐시 키에 포함할 요소를 정의합니다.
15. 참고 자료
- Nx Documentation — Nx 공식 문서
- Turborepo Documentation — Turborepo 공식 문서
- Changesets Documentation — Changesets 공식 문서
- pnpm Workspace — pnpm 워크스페이스 공식 문서
- Google Monorepo Paper — Why Google Stores Billions of Lines of Code in a Single Repository
- Lerna Documentation — Lerna 공식 문서
- Rush Documentation — Rush 공식 문서 (Microsoft)
- Monorepo Explained — 모노레포 도구 비교 사이트
- Turborepo Caching — 캐싱 메커니즘 상세
- Nx Affected — Affected 명령 동작 원리
- Git Sparse Checkout — 대규모 저장소 최적화
- CODEOWNERS Syntax — GitHub CODEOWNERS 문법
Monorepo Strategy Guide 2025: Nx vs Turborepo vs Lerna — Complete Guide to Managing Large Codebases
- Introduction: Why Monorepo
- 1. Monorepo vs Polyrepo
- 2. Monorepo at Scale
- 3. Tool Comparison: Nx vs Turborepo vs Lerna vs Rush
- 4. pnpm Workspace Setup
- 5. Nx Deep Dive
- 6. Turborepo Deep Dive
- 7. Shared Library Strategies
- 8. Versioning and Changesets
- 9. CI/CD Optimization
- 10. CODEOWNERS and Team Boundaries
- 11. Migration from Polyrepo to Monorepo
- 12. Common Pitfalls and Solutions
- 13. Interview Questions (10 Questions)
- Q1. Explain the differences, advantages, and disadvantages of monorepo vs polyrepo.
- Q2. What are the key differences between Nx and Turborepo?
- Q3. Explain how affected commands work.
- Q4. How does remote caching improve CI performance?
- Q5. What is the workspace:* protocol in pnpm workspaces?
- Q6. Describe the Changesets workflow.
- Q7. Why is CODEOWNERS important in a monorepo?
- Q8. How do you optimize Docker builds in a monorepo?
- Q9. What should you watch out for when migrating from polyrepo to monorepo?
- Q10. What role do Nx module boundaries play in a monorepo?
- 14. Quiz (5 Questions)
- 15. References
Introduction: Why Monorepo
Google manages over 2 billion lines of code in a single repository. Meta, Microsoft, Uber, and Airbnb also use monorepos. Monorepos are no longer an experimental strategy but a proven pattern for large-scale software development.
However, poorly managed monorepos lead to CI taking over 30 minutes, dependency hell, and unclear code ownership. This article covers everything from proper adoption strategies to in-depth Nx, Turborepo, and Lerna comparisons, CI/CD optimization, and team management.
1. Monorepo vs Polyrepo
1.1 Definitions
- Monorepo: Managing multiple projects/packages in a single repository
- Polyrepo: Managing each project in a separate repository (also called multi-repo)
1.2 Comparison Table
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Code sharing | Immediate (same repo) | Via npm/registry |
| Dependency management | Unified | Independent per repo |
| Atomic changes | Possible (single PR) | Impossible (multiple PRs) |
| CI complexity | High (optimization needed) | Low (independent repos) |
| Code visibility | Searchable across all code | Separate search per repo |
| Release management | Complex (Changesets etc.) | Simple (individual versions) |
| Permission management | CODEOWNERS needed | Separated by repo |
| Initial clone | Can be slow | Fast |
| Refactoring | Possible across entire codebase | Difficult at repo boundaries |
1.3 When to Choose a Monorepo
Monorepo is suitable when:
- There are tight dependencies between packages
- Many shared libraries exist
- Atomic changes are needed
- You want to maximize code reuse
- Cross-team code visibility is important
Polyrepo is suitable when:
- Projects are completely independent
- Teams use very different tech stacks
- Strict access control is required
- Open-source and private code must be separated
2. Monorepo at Scale
2.1 Google (Piper)
- Scale: Over 2 billion lines, 86TB
- Tools: Piper (custom VCS) + Blaze/Bazel (build)
- Key insight: All code in one repository, trunk-based development
- Lesson: Impossible without proper tooling. Custom VCS and build systems are essential
2.2 Meta (Buck2)
- Tools: Mercurial + Buck2 (build system)
- Key insight: Virtual filesystem loads only needed files
- Lesson: At massive scale, filesystem-level optimization is necessary
2.3 Microsoft (1JS)
- Tools: Git + Rush (build orchestration)
- Key insight: Unified management of JavaScript/TypeScript projects
- Lesson: Incremental migration is the realistic strategy
2.4 Uber
- Tools: Go monorepo + Buck (build)
- Key insight: Over 5000 microservices in a single repository
- Lesson: Microservices and monorepo can coexist
3. Tool Comparison: Nx vs Turborepo vs Lerna vs Rush
3.1 Feature Comparison Table
| Feature | Nx | Turborepo | Lerna | Rush |
|---|---|---|---|---|
| Task pipeline | Yes | Yes | Yes (v7+) | Yes |
| Local caching | Yes | Yes | No | Yes |
| Remote caching | Yes (Nx Cloud) | Yes (Vercel) | No | Yes (self-hosted) |
| Affected detection | Yes (project graph) | Yes (file hashing) | Yes (v7+) | Yes |
| Code generators | Yes (generators) | No | No | No |
| Project graph visualization | Yes | No | No | No |
| Framework plugins | Yes (React, Angular etc.) | No | No | No |
| Distributed execution | Yes (Nx Agents) | No | No | Yes |
| Package managers | npm, yarn, pnpm | npm, yarn, pnpm | npm, yarn, pnpm | pnpm |
| Learning curve | High | Low | Low | Medium |
| Open source | Yes | Yes | Yes | Yes |
3.2 Tool Selection Guide
Starting a project?
├── Small (fewer than 10 packages)
│ ├── Want quick start → Turborepo
│ └── Need code generation → Nx
├── Medium (10-50 packages)
│ ├── Vercel/Next.js ecosystem → Turborepo
│ ├── Need rich plugins → Nx
│ └── Already using Lerna → Lerna v7
└── Large (50+ packages)
├── Need distributed execution → Nx
└── Microsoft-style → Rush
4. pnpm Workspace Setup
4.1 Basic Structure
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 Root 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 Cross-Package References
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*"
}
}
workspace:* directly references local packages. When publishing to npm, pnpm replaces it with the actual version.
4.5 .npmrc Configuration
# Hoisting settings
shamefully-hoist=false
strict-peer-dependencies=false
# Workspace settings
link-workspace-packages=true
prefer-workspace-packages=true
5. Nx Deep Dive
5.1 Nx Initial Setup
# Create new Nx workspace
npx create-nx-workspace@latest my-monorepo --preset=ts
# Add Nx to existing monorepo
npx nx@latest init
5.2 nx.json Configuration
{
"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 Project Graph
# Visualize project dependency graph
npx nx graph
# Focus on a specific project
npx nx graph --focus=my-app
# View affected projects
npx nx affected:graph
Project graph example:
web-app ───▶ ui-lib ───▶ utils
│ │
▼ ▼
api-app ───▶ shared-types
5.4 Affected Commands (Change Detection)
# Build only projects affected by changes
npx nx affected -t build
# Test only affected projects
npx nx affected -t test
# Specify base branch
npx nx affected -t build --base=main --head=HEAD
How Affected works:
- Detect changed files via Git diff
- Find projects containing those files in the project graph
- Trace the dependency graph to identify all affected projects
- Run tasks only for those projects
5.5 Generators (Code Generation)
# Generate React component
npx nx generate @nx/react:component Button --project=ui
# Generate library
npx nx generate @nx/js:library shared-utils
# Create custom generator
npx nx generate @nx/plugin:generator my-generator --project=tools
5.6 Nx Cloud (Remote Caching + Distributed Execution)
# Connect to Nx Cloud
npx nx connect
# CI configuration with Nx
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 Deep Dive
6.1 Turborepo Initial Setup
# Create new Turborepo project
npx create-turbo@latest
# Add Turborepo to existing monorepo
pnpm add -D turbo -w
6.2 turbo.json Configuration
{
"$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 How Task Pipelines Work
When running turbo run build:
1. Dependency graph analysis
utils (no deps) → ui (depends on utils) → web (depends on ui, utils)
2. Parallel execution
[utils: build] ──done──▶ [ui: build] ──done──▶ [web: build]
[api: build] ──────────┘
(depends only on utils)
3. Caching
- Calculate hash of input files
- If hash matches previous build, restore from cache
- Build time: 5min → 0.1sec (cache hit)
6.4 Caching Mechanism
# Local cache location
ls node_modules/.cache/turbo/
# Check cache status
turbo run build --dry-run
# Invalidate cache
turbo run build --force
# Enable remote caching
npx turbo login
npx turbo link
6.5 Filtering and Scoping
# Build specific package only
turbo run build --filter=@myorg/web
# Build only changed packages
turbo run build --filter=...[HEAD^1]
# Build specific package and its dependencies
turbo run build --filter=@myorg/web...
# Packages in a specific directory
turbo run build --filter=./apps/*
6.6 Remote Caching Setup
# Vercel Remote Cache (official)
npx turbo login
npx turbo link
Environment variable configuration:
TURBO_TOKEN=your-token
TURBO_TEAM=your-team
TURBO_API=https://your-cache-server.com
7. Shared Library Strategies
7.1 Package Classification
packages/
├── ui/ # UI components (Button, Modal, Form)
├── utils/ # Utilities (date, string, validation)
├── types/ # Shared TypeScript types
├── config/ # Shared config (ESLint, TypeScript, Prettier)
├── hooks/ # Shared React hooks
├── api-client/ # API client (type-safe fetch)
└── constants/ # Constants (error codes, route paths)
7.2 Internal Package Pattern
Pattern that directly references TypeScript source without building:
{
"name": "@myorg/ui",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
Set up path aliases in the consuming app's tsconfig.json:
{
"compilerOptions": {
"paths": {
"@myorg/ui": ["../../packages/ui/src"],
"@myorg/ui/*": ["../../packages/ui/src/*"]
}
}
}
7.3 Built Package Pattern
Packages published to npm require separate builds:
{
"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. Versioning and Changesets
8.1 Introduction to Changesets
Changesets automates versioning and changelog generation in monorepos.
# Installation
pnpm add -D @changesets/cli -w
# Initialize
pnpm changeset init
8.2 Workflow
# 1. Add change description
pnpm changeset
# ? Which packages changed? → @myorg/ui, @myorg/utils
# ? Change type? → minor (new feature)
# ? Description? → Added new Button variant
# 2. File created in .changeset/ directory
cat .changeset/brave-dogs-run.md
# ---
# "@myorg/ui": minor
# "@myorg/utils": patch
# ---
#
# Added new Button variant
# 3. Version update (run in CI)
pnpm changeset version
# 4. Publish
pnpm changeset publish
8.3 Configuration (.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: Package groups whose versions are bumped together
- fixed: Package groups that always share the same version
- ignore: Packages excluded from publishing (apps, etc.)
8.4 Independent vs Fixed Versioning
| Strategy | Description | Example |
|---|---|---|
| Independent | Each package has its own version | ui@2.3.0, utils@1.5.0 |
| Fixed | All packages share the same version | ui@3.0.0, utils@3.0.0 |
| Linked | Related packages version together | ui@2.3.0 bump triggers hooks@2.3.0 |
9. CI/CD Optimization
9.1 Affected Build Strategy
Only build and test projects affected by changed files.
# .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: build only changed packages
- run: turbo run build test lint --filter=...[origin/main]
# Or Nx: affected projects only
# - run: npx nx affected -t build test lint
9.2 CI Time Reduction with Remote Caching
Without cache:
lint (2min) + test (5min) + build (8min) = 15min
Remote cache hit:
lint (0.1s) + test (0.2s) + build (0.3s) = 0.6s
Only actually changed packages:
lint (20s) + test (1min) + build (2min) = 3min 20s
9.3 Parallel Execution Strategy
# Matrix strategy for parallel execution
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 Build Optimization
# Docker build in a monorepo
FROM node:20-slim AS base
RUN corepack enable
FROM base AS pruned
WORKDIR /app
COPY . .
# turbo prune: extract specific app and its dependencies
RUN npx turbo prune @myorg/api --docker
FROM base AS installer
WORKDIR /app
# Install dependencies first (cache layer)
COPY /app/out/json/ .
COPY /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/api
FROM base AS runner
WORKDIR /app
COPY /app/apps/api/dist ./dist
COPY /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]
10. CODEOWNERS and Team Boundaries
10.1 CODEOWNERS File
# .github/CODEOWNERS
# Global default owners
* @org/platform-team
# App-specific owners
/apps/web/ @org/frontend-team
/apps/api/ @org/backend-team
/apps/mobile/ @org/mobile-team
# Package-specific owners
/packages/ui/ @org/design-system-team
/packages/utils/ @org/platform-team
/packages/auth/ @org/security-team
# Configuration files
/tooling/ @org/dx-team
/.github/ @org/dx-team
/turbo.json @org/dx-team
10.2 Team Boundaries with 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"]
}
]
}
]
}
}
11. Migration from Polyrepo to Monorepo
11.1 Step-by-Step Migration
Step 1: Preparation
# Create new monorepo repository
mkdir my-monorepo && cd my-monorepo
git init
pnpm init
Step 2: Move with Git History Preserved
# Move existing repo as subdirectory (preserving history)
git remote add -f web-repo https://github.com/org/web-app.git
git merge web-repo/main --allow-unrelated-histories
# Restructure directory using git-filter-repo
git filter-repo --to-subdirectory-filter apps/web
Step 3: Workspace Configuration
# Set up pnpm workspace
cat > pnpm-workspace.yaml << 'EOF'
packages:
- "apps/*"
- "packages/*"
EOF
Step 4: Extract Shared Packages
# Extract duplicate code into shared packages
mkdir -p packages/shared-utils/src
# Move common utilities and configure packages
Step 5: Update CI/CD
# Set up Turborepo or Nx
pnpm add -D turbo -w
# Configure turbo.json (see section 6.2)
11.2 Incremental Migration Strategy
Do not move all repositories at once. Recommended order:
- Move shared libraries first
- Move the most heavily depended-upon app
- Move remaining apps sequentially
- Validate CI/CD at each step
12. Common Pitfalls and Solutions
12.1 Slow CI
Problem: Building/testing all packages every time, CI takes over 30 minutes
Solution:
- Use affected commands (build only what changed)
- Enable remote caching
- Configure parallel execution
# Before: build all packages (15min)
pnpm -r run build
# After: only changed packages (2min)
turbo run build --filter=...[origin/main]
12.2 Dependency Hell
Problem: Package A uses lodash@4 while Package B uses lodash@3
Solution:
- Use pnpm's strict mode (prevents phantom dependencies)
- Manage shared dependencies at the root
- Use
syncpackto synchronize versions
# Check dependency version mismatches with syncpack
npx syncpack list-mismatches
npx syncpack fix-mismatches
12.3 Unclear Code Ownership
Problem: Unclear who is responsible for which code
Solution:
- Set up CODEOWNERS file (see section 10.1)
- Enforce dependency restrictions with Nx module boundaries
- Use team-specific package namespaces
12.4 Initial Clone Time
Problem: git clone of a large monorepo takes over 10 minutes
Solution:
# Shallow clone
git clone --depth 1 https://github.com/org/monorepo.git
# Partial clone (without blobs)
git clone --filter=blob:none https://github.com/org/monorepo.git
# Sparse checkout (specific directories only)
git clone --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set apps/web packages/ui
12.5 Build Order Issues
Problem: Package A depends on Package B, but B has not been built yet
Solution: Configure dependsOn in the task pipeline
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
^build means run the build task of the current package's dependencies first.
13. Interview Questions (10 Questions)
Q1. Explain the differences, advantages, and disadvantages of monorepo vs polyrepo.
Model answer: Monorepo manages multiple projects in a single repository, enabling easy code sharing and atomic changes, but requires CI optimization. Polyrepo manages each project in an independent repository, providing high independence but making code sharing difficult and cross-repository changes complex.
Q2. What are the key differences between Nx and Turborepo?
Model answer: Nx provides rich features including project graph visualization, code generators, framework plugins, and distributed execution, but has a steep learning curve. Turborepo focuses on caching and task pipelines with simple configuration, but lacks code generation and visualization features.
Q3. Explain how affected commands work.
Model answer: Changed files are detected via Git diff, then the project graph identifies which projects contain those files and all projects that depend on them. Only those projects are built/tested. This can reduce CI time by over 90%.
Q4. How does remote caching improve CI performance?
Model answer: Build inputs (source code, configuration, etc.) are hashed, and if the inputs are identical, previous build results are restored from a cloud cache. When multiple developers build the same code or CI runs repeated builds, the build can be skipped entirely.
Q5. What is the workspace:* protocol in pnpm workspaces?
Model answer: workspace:* is a protocol that directly references packages in the local workspace. They are connected via symlinks, so source changes are reflected immediately without building. When publishing to npm, pnpm automatically replaces it with the actual version number.
Q6. Describe the Changesets workflow.
Model answer: Developers run pnpm changeset to describe changes, creating a markdown file in the .changeset/ directory. After PR merge, CI runs changeset version to update package versions and changeset publish to publish to npm.
Q7. Why is CODEOWNERS important in a monorepo?
Model answer: In a monorepo, code from multiple teams exists in one repository, potentially making code ownership unclear. The CODEOWNERS file assigns responsible teams per directory, automatically assigning reviewers to PRs to ensure code quality and accountability.
Q8. How do you optimize Docker builds in a monorepo?
Model answer: Use Turborepo's turbo prune to extract a specific app and its dependencies, then build in Docker. Separate dependency installation and source building in a multi-stage build to leverage Docker layer caching.
Q9. What should you watch out for when migrating from polyrepo to monorepo?
Model answer: Preserving Git history is important. git-filter-repo can maintain history when moving to subdirectories. Do not migrate all repositories at once; move shared libraries first incrementally, validating CI/CD at each step.
Q10. What role do Nx module boundaries play in a monorepo?
Model answer: They restrict dependencies between packages via ESLint rules. Projects are assigned tags, and rules define which tagged projects can reference which. For example, preventing a frontend app from importing a backend-only package.
14. Quiz (5 Questions)
Q1. What does "dependsOn": ["^build"] mean in turbo.json?
Answer: The ^ prefix means run the build tasks of the current package's dependencies first. For example, if Package A depends on Package B, B's build completes before A's build starts. Without ^, ["build"] would mean a task dependency within the same package.
Q2. Why is shamefully-hoist=false important in pnpm monorepos?
Answer: pnpm installs packages in an isolated node_modules structure by default, preventing phantom dependencies (using undeclared dependencies). shamefully-hoist=false maintains this strict isolation. Enabling hoisting would allow access to undeclared packages, potentially causing issues during deployment.
Q3. What is the difference between linked and fixed in Changesets?
Answer: linked means when one package in the group gets a version bump, the rest bump to the same level (e.g., if one gets a minor bump, others do too). fixed means all packages in the group always share the exact same version number (e.g., all go from 3.0.0 to 3.1.0).
Q4. Why is git clone --filter=blob:none useful for monorepos?
Answer: Partial clone skips downloading blobs (file content) initially, fetching them on demand. This dramatically reduces initial clone time for large monorepos. Git only fetches blobs for checked-out files, so combined with sparse checkout, only necessary files are minimally downloaded.
Q5. How are cache keys determined in remote caching?
Answer: All task inputs including source files, configuration files, environment variables, and dependency build results are hashed to generate the cache key. Identical inputs produce identical hashes, allowing restoration of previously built results. Turborepo uses inputs and env settings in turbo.json to define which elements are included in the cache key.
15. References
- Nx Documentation — Official Nx docs
- Turborepo Documentation — Official Turborepo docs
- Changesets Documentation — Official Changesets docs
- pnpm Workspace — Official pnpm workspace docs
- Google Monorepo Paper — Why Google Stores Billions of Lines of Code in a Single Repository
- Lerna Documentation — Official Lerna docs
- Rush Documentation — Official Rush docs (Microsoft)
- Monorepo Explained — Monorepo tool comparison site
- Turborepo Caching — Caching mechanism details
- Nx Affected — How affected commands work
- Git Sparse Checkout — Large repository optimization
- CODEOWNERS Syntax — GitHub CODEOWNERS syntax