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

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며: 왜 모노레포인가
- 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 문법