멀티레포의 고통
대부분의 프로젝트는 하나의 저장소 (repository) 에서 시작합니다. 그런데 프로젝트가 성장하면서 관리자 페이지 , 디자인 시스템 , 공통 유틸리티 같은 별도 프로젝트가 생기기 시작합니다.
각각을 독립된 저장소로 관리하는 것이 멀티레포입니다.
github.com/team/web-app ← 사용자 앱
github.com/team/admin-app ← 관리자 앱
github.com/team/shared-ui ← 공통 컴포넌트 (npm 배포)
github.com/team/shared-utils ← 공통 유틸리티 (npm 배포)처음엔 깔끔해 보이지만, 시간이 지나면 다음 문제들이 쌓입니다.
코드 중복
formatDate, cn() 같은 유틸리티가 여러 저장소에 복사됩니다. shared 패키지로 추출해도, 변경할 때마다 npm에 배포하고 → 각 저장소에서 업데이트하는 루프가 필요합니다.
버전 드리프트
web-app은 React 18, admin-app은 React 19를 쓰는 상황이 발생합니다. 공통 컴포넌트가 둘 다 지원해야 하는 부담이 생깁니다.
CI/CD 파편화
저장소마다 GitHub Actions 워크플로우를 따로 관리합니다. eslint 설정, tsconfig, 배포 스크립트가 조금씩 다릅니다. 하나를 고치면 나머지도 고쳐야 하는데, 잊기 쉽습니다.
원자적 변경의 어려움
shared-ui의 API를 변경하면, web-app과 admin-app도 함께 수정해야 합니다. 멀티레포에서는 3개의 PR을 각각 만들고, 배포 순서를 조율해야 합니다.
모노레포의 장점
모노레포는 여러 프로젝트를 하나의 Git 저장소 에 넣는 구조입니다.
monorepo/
apps/
web/ ← 사용자 앱
admin/ ← 관리자 앱
packages/
ui/ ← 공통 컴포넌트
utils/ ← 공통 유틸리티
tsconfig/ ← 공유 설정
package.json ← 루트
turbo.json ← Turborepo 설정멀티레포의 고통이 해소됩니다.
| 문제 | 멀티레포 | 모노레포 |
|---|---|---|
| 코드 공유 | npm 배포 필요 | 로컬 링크 (workspace) |
| 버전 통일 | 수동 관리 | 루트에서 일괄 관리 |
| CI/CD | 저장소별 파편화 | 단일 파이프라인 |
| 원자적 변경 | PR 3개 + 배포 조율 | PR 1개로 모든 패키지 수정 |
Workspaces
모노레포의 핵심은 패키지 매니저의 workspace 기능입니다. 패키지 간 의존성을 로컬 파일시스템에서 직접 링크합니다.
npm workspaces
// 루트 package.json
{
"workspaces": ["apps/*", "packages/*"]
}pnpm workspaces
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'Yarn workspaces
// 루트 package.json
{
"workspaces": ["apps/*", "packages/*"]
}workspace를 설정하면, 패키지에서 다른 패키지를 의존성으로 선언할 수 있습니다.
// apps/web/package.json
{
"dependencies": {
"@mono/ui": "workspace:*",
"@mono/utils": "workspace:*"
}
}workspace:*는 npm 레지스트리 대신 로컬 패키지를 직접 링크 합니다. @mono/ui를 수정하면 web에서 즉시 반영됩니다.
Turborepo, 태스크 캐싱과 병렬 실행
workspace만으로는 모노레포를 효율적으로 운영하기 어렵습니다. 패키지가 30개인 모노레포에서 npm run build를 하면, 변경 없는 패키지까지 전부 빌드합니다.
Turborepo는 이 문제를 해결하는 빌드 시스템입니다.
태스크 그래프
Turborepo는 turbo.json을 분석하여 패키지 간 의존 관계를 파악하고, 태스크 그래프 를 만듭니다.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}"dependsOn": ["^build"]에서 ^는 의존하는 패키지의 build가 먼저 실행되어야 한다 는 뜻입니다.
turbo run build 실행 시:
@mono/utils:build @mono/tsconfig:build
↓ ↓
@mono/ui:build ←┘
↓
apps/web:build apps/admin:build (병렬)독립적인 태스크는 자동으로 병렬 실행됩니다.
캐싱 메커니즘
Turborepo는 각 태스크에 대해 fingerprint (해시)를 계산합니다. 입력이 같으면 결과도 같다는 원리입니다.
fingerprint에 포함되는 요소:
- 소스 파일의 내용
- 의존성 버전
- 환경 변수
turbo.json설정
$ turbo run build
# @mono/utils:build, cache hit, replaying output ← 0.1초
# @mono/ui:build, cache hit, replaying output ← 0.1초
# apps/web:build, cache miss, executing ← 12초
# apps/admin:build, cache hit, replaying output ← 0.1초캐시 히트 시, 이전 빌드 결과 (dist/ 폴더)를 그대로 복원합니다. 로그 출력까지 재생합니다.
리모트 캐시
로컬 캐시만으로는 CI 환경에서 의미가 없습니다. 매번 새 머신에서 실행하니까요. Turborepo는 리모트 캐시 를 지원합니다.
# Vercel Remote Cache 연결
npx turbo login
npx turbo link팀원 A가 빌드한 결과를 팀원 B와 CI 서버가 공유합니다. 한 사례에서는 리모트 캐시 도입으로 Turbo 태스크 시간이 약 50% 감소했습니다.
Nx, Affected와 프로젝트 그래프
Nx는 Turborepo와 비슷한 목표를 가진 빌드 시스템이지만, 몇 가지 차별점이 있습니다.
Affected 명령어
Nx의 가장 강력한 기능은 nx affected입니다. Git diff와 프로젝트 그래프를 결합하여 변경된 패키지와 그에 의존하는 패키지만 작업을 실행합니다.
# shared를 수정한 후
nx affected --target=build
# ✓ shared:build ← 변경됨
# ✓ web:build ← shared에 의존 → 영향받음
# ✓ admin:build ← shared에 의존 → 영향받음
# - docs:build ← shared에 의존하지 않음 → 스킵프로세스는 다음과 같습니다.
- Git으로 변경된 파일을 감지
- 프로젝트 그래프에서 해당 파일이 속한 패키지를 찾음
- 그 패키지에 의존하는 모든 패키지를 추적
- 추적된 패키지만 태스크 실행
Nx vs Turborepo
| 특성 | Turborepo | Nx |
|---|---|---|
| 설정 복잡도 | 낮음 (turbo.json 하나) | 높음 (프로젝트별 설정) |
| 캐싱 | 로컬 + Vercel 리모트 | 로컬 + Nx Cloud 리모트 |
| Affected | 없음 (전체 빌드 + 캐시) | Git diff 기반 영향 분석 |
| 코드 생성 | 없음 | 풍부한 generator 지원 |
| 플러그인 | 최소 | React, Angular, Next.js 등 |
| 학습 곡선 | 낮음 | 중간~높음 |
Turborepo는 "기존 모노레포에 캐싱을 얹는" 가벼운 접근이고, Nx는 "모노레포 전체를 관리하는" 포괄적 접근입니다.
모노레포의 단점
모노레포가 만능은 아닙니다.
- 저장소 크기 , 코드가 한 곳에 모이므로 Git 저장소가 커집니다.
git clone이 느려질 수 있습니다 (shallow clone으로 완화) - CI 복잡도 , 변경 감지, 선택적 빌드, 캐싱 설정이 필요합니다. Turborepo/Nx 없이는 모든 패키지를 매번 빌드하게 됩니다
- 권한 관리 , GitHub은 저장소 단위로 권한을 설정합니다. 모노레포에서는
CODEOWNERS파일로 디렉토리별 리뷰어를 지정해야 합니다 - 의존성 충돌 , 패키지 A는 React 18, 패키지 B는 React 19를 원하는 경우. 모노레포에서는 버전을 통일하거나, pnpm의
overrides로 관리해야 합니다
시각화
멀티레포에서 모노레포로, 그리고 Turborepo와 Nx의 최적화 전략을 단계별로 봅니다.
프로젝트마다 별도 저장소를 운영합니다. 독립적이지만, 공통 코드가 중복되고 의존성 버전이 서로 달라지는 버전 드리프트가 발생합니다.
체크리스트
- 2개 이상의 프로젝트가 코드를 공유하고 있다면, 모노레포를 검토했는가?
- workspace 설정으로 패키지 간 로컬 링크가 동작하는가?
- Turborepo나 Nx로 태스크 캐싱을 설정했는가?
- CI에서 리모트 캐시를 활용하고 있는가?
-
CODEOWNERS로 디렉토리별 리뷰어를 지정했는가? - 의존성 버전이 패키지 간에 일관적인가?