
모노레포(Monorepo)는 여러 프로젝트를 하나의 저장소에서 관리하는 방식이다. 코드 공유, 일관성 유지, 대규모 프로젝트 관리에 효과적이다. 이 글은 모노레포의 개념부터 Turborepo, Nx 등 주요 도구를 활용한 실전 구축까지 완벽하게 정리한다.
1. 모노레포란 무엇인가?
1-1. 기본 개념
모노레포(Monorepo)는 “Monolithic Repository”의 줄임말로, 여러 프로젝트나 패키지를 하나의 Git 저장소에서 관리하는 방식이다.
전통적인 멀티레포 (Multi-repo):
company/
├── frontend/ (별도 저장소)
├── backend/ (별도 저장소)
├── shared-library/ (별도 저장소)
└── mobile/ (별도 저장소)모노레포 (Monorepo):
company/
├── apps/
│ ├── frontend/
│ ├── backend/
│ └── mobile/
├── packages/
│ ├── shared-ui/
│ ├── shared-utils/
│ └── shared-types/
└── package.json1-2. 모노레포의 장단점
장점:
- 코드 공유 용이: 패키지 간 코드 공유가 쉬움
- 일관성 유지: 공통 설정, 린팅 규칙 통일
- 원자적 변경: 여러 패키지를 한 커밋으로 변경
- 리팩토링 용이: 전체 코드베이스에서 안전하게 리팩토링
- 의존성 관리: 버전 충돌 방지
단점:
- 저장소 크기: 큰 저장소로 인한 클론 시간 증가
- 권한 관리: 세밀한 권한 제어 어려움
- 빌드 복잡도: 빌드 시스템 복잡도 증가
- CI/CD 복잡도: 변경된 패키지만 빌드하는 로직 필요
1-3. 모노레포 사용 사례
적합한 경우:
- 여러 관련 프로젝트 관리
- 코드 공유가 많은 프로젝트
- 대규모 팀 협업
- 마이크로서비스 아키텍처
부적합한 경우:
- 완전히 독립적인 프로젝트
- 작은 규모의 프로젝트
- 외부 오픈소스 프로젝트
2. 주요 모노레포 도구 비교
2-1. 도구 개요
| 도구 | 특징 | 장점 | 단점 |
|---|---|---|---|
| Turborepo | Vercel 개발, 빌드 시스템 | 빠른 빌드, 캐싱 우수 | 상대적으로 새로움 |
| Nx | Nrwl 개발, 엔터프라이즈급 | 강력한 기능, 플러그인 | 학습 곡선 높음 |
| Lerna | 전통적인 도구 | 간단함, 널리 사용 | 성능 제한적 |
| pnpm workspaces | 패키지 매니저 통합 | 간단한 설정 | 고급 기능 제한적 |
| Yarn workspaces | Yarn 통합 | 안정적 | Yarn에 종속 |
2-2. 선택 가이드
Turborepo 추천:
- 빠른 빌드 속도 중요
- 간단한 설정 선호
- Next.js, React 프로젝트
Nx 추천:
- 대규모 엔터프라이즈 프로젝트
- 강력한 도구 필요
- 다양한 프레임워크 지원 필요
Lerna 추천:
- 기존 프로젝트 마이그레이션
- 간단한 패키지 관리만 필요
3. Turborepo 실전 가이드
3-1. 프로젝트 구조
기본 구조:
my-monorepo/
├── apps/
│ ├── web/ # Next.js 앱
│ ├── admin/ # React 앱
│ └── api/ # Node.js API
├── packages/
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── utils/ # 유틸리티 함수
│ └── config/ # 공유 설정
├── package.json
├── turbo.json # Turborepo 설정
└── pnpm-workspace.yaml # pnpm workspaces 설정3-2. 초기 설정
루트 package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^1.10.0"
}
}turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}3-3. 앱 생성
apps/web/package.json:
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@my-monorepo/ui": "workspace:*",
"@my-monorepo/utils": "workspace:*"
}
}apps/web/next.config.js:
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
const withVanillaExtract = createVanillaExtractPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@my-monorepo/ui'],
};
module.exports = withVanillaExtract(nextConfig);3-4. 패키지 생성
packages/ui/package.json:
{
"name": "@my-monorepo/ui",
"version": "1.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"typescript": "^5.0.0"
}
}packages/ui/Button.tsx:
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({ children, onClick }) => {
return (
<button
onClick={onClick}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
{children}
</button>
);
};packages/ui/index.tsx:
export { Button } from './Button';
export type { ButtonProps } from './Button';3-5. 패키지 사용
apps/web/app/page.tsx:
import { Button } from '@my-monorepo/ui';
import { formatDate } from '@my-monorepo/utils';
export default function Home() {
return (
<div>
<h1>Welcome</h1>
<p>Today is {formatDate(new Date())}</p>
<Button onClick={() => alert('Clicked!')}>
Click Me
</Button>
</div>
);
}3-6. 빌드 및 실행
개발 모드:
# 모든 앱 실행
pnpm dev
# 특정 앱만 실행
pnpm --filter web dev빌드:
# 모든 패키지 빌드
pnpm build
# 특정 패키지만 빌드
pnpm --filter web build캐싱:
- Turborepo는 자동으로 빌드 결과를 캐싱
- 변경되지 않은 패키지는 재빌드하지 않음
- CI/CD에서도 캐싱 활용 가능
4. Nx 실전 가이드
4-1. 프로젝트 초기화
Nx 설치:
npx create-nx-workspace@latest my-monorepo기본 구조:
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
├── libs/
│ ├── ui/
│ └── utils/
├── nx.json
├── workspace.json
└── package.json4-2. 앱 생성
Next.js 앱 생성:
nx generate @nx/next:application webReact 라이브러리 생성:
nx generate @nx/react:library ui --directory=libs/ui4-3. 태스크 실행
개발 서버:
nx serve web빌드:
nx build web테스트:
nx test web의존성 그래프:
nx graph4-4. 영향을 받는 프로젝트만 빌드
영향 분석:
# 변경된 프로젝트 확인
nx affected:graph
# 영향받는 프로젝트만 빌드
nx affected:build
# 영향받는 프로젝트만 테스트
nx affected:test5. 패키지 간 의존성 관리
5-1. 내부 패키지 참조
package.json에서:
{
"dependencies": {
"@my-monorepo/ui": "workspace:*",
"@my-monorepo/utils": "workspace:*"
}
}TypeScript 경로 매핑:
{
"compilerOptions": {
"paths": {
"@my-monorepo/ui": ["./packages/ui"],
"@my-monorepo/utils": ["./packages/utils"]
}
}
}5-2. 버전 관리
버전 동기화:
- 모든 패키지 버전을 동일하게 유지
- 또는 독립적으로 버전 관리
Lerna를 사용한 버전 관리:
{
"version": "independent",
"packages": ["packages/*", "apps/*"]
}6. CI/CD 통합
6-1. GitHub Actions (Turborepo)
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Test
run: pnpm test6-2. 변경된 패키지만 빌드
Turborepo:
- name: Build changed packages
run: |
pnpm turbo build --filter='...[origin/main]'Nx:
- name: Build affected
run: |
nx affected:build --base=origin/main7. 공유 설정 관리
7-1. ESLint 설정
packages/eslint-config/package.json:
{
"name": "@my-monorepo/eslint-config",
"version": "1.0.0",
"main": "index.js"
}packages/eslint-config/index.js:
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
],
rules: {
// 공통 규칙
},
};앱에서 사용:
{
"extends": "@my-monorepo/eslint-config"
}7-2. TypeScript 설정
packages/tsconfig/base.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}앱에서 확장:
{
"extends": "@my-monorepo/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist"
}
}8. Best Practices
8-1. 프로젝트 구조
명확한 구조:
apps/: 실행 가능한 애플리케이션packages/: 재사용 가능한 패키지tools/: 개발 도구 및 스크립트
8-2. 의존성 관리
의존성 최소화:
- 필요한 패키지만 설치
- 공통 의존성은 루트에 설치
버전 통일:
- 동일한 라이브러리는 동일한 버전 사용
- 버전 충돌 방지
8-3. 빌드 최적화
병렬 빌드:
- Turborepo, Nx는 자동으로 병렬 빌드
- 의존성 그래프 기반 최적화
캐싱 활용:
- 빌드 결과 캐싱
- CI/CD에서도 캐싱 활용
8-4. 코드 공유
공유 패키지 설계:
- 명확한 인터페이스 정의
- 독립적으로 테스트 가능하도록 설계
- 문서화
9. 마이그레이션 가이드
9-1. 기존 프로젝트 통합
단계별 마이그레이션:
- 모노레포 구조 생성
- 기존 프로젝트 이동
- 의존성 정리
- 빌드 시스템 설정
- CI/CD 업데이트
주의사항:
- Git 히스토리 보존
- 점진적 마이그레이션
- 팀과 충분한 소통
FAQ
Q: 모노레포가 항상 좋은가요?
A: 상황에 따라 다릅니다. 코드 공유가 많고 관련된 프로젝트를 관리할 때 유리하지만, 완전히 독립적인 프로젝트는 멀티레포가 나을 수 있습니다.
Q: Turborepo와 Nx 중 어떤 것을 선택해야 하나요?
A: 간단한 프로젝트와 빠른 빌드가 중요하면 Turborepo, 대규모 엔터프라이즈 프로젝트와 강력한 도구가 필요하면 Nx를 선택하세요.
Q: 모노레포에서 Git 히스토리는 어떻게 관리하나요?
A: 기존 프로젝트를 통합할 때는 git subtree나 git filter-branch를 사용하여 히스토리를 보존할 수 있습니다.
Q: 패키지 버전을 독립적으로 관리할 수 있나요?
A: 네, Lerna의 independent 모드나 각 패키지의 독립적인 버전 관리가 가능합니다.
Q: CI/CD에서 모든 패키지를 빌드하나요?
A: 아니요, 변경된 패키지만 빌드하는 것이 효율적입니다. Turborepo와 Nx는 이를 자동으로 처리합니다.
Q: 모노레포 저장소 크기가 커지면 어떻게 하나요?
A: Git LFS 사용, 부분 클론, 또는 저장소 분할을 고려할 수 있습니다. 하지만 대부분의 경우 모던 Git은 큰 저장소도 잘 처리합니다.
결론: 모노레포로 대규모 프로젝트 관리
모노레포는 대규모 프로젝트와 팀 협업에서 강력한 도구다. 코드 공유, 일관성 유지, 효율적인 빌드 등 많은 장점을 제공한다.
하지만 모든 프로젝트에 적합한 것은 아니다. 프로젝트의 규모, 팀 구조, 코드 공유 정도를 고려하여 선택해야 한다. 적절히 활용하면 개발 생산성을 크게 향상시킬 수 있다.
Turborepo와 Nx 같은 도구들이 모노레포의 복잡성을 크게 줄여주고 있다. 이제는 더 많은 프로젝트가 모노레포를 채택하고 있으며, 이는 앞으로의 트렌드가 될 것이다.
모노레포 학습 후 프로젝트 기회
모노레포를 마스터한 후, 실제 프로젝트에 적용하고 싶다면 블루버튼 같은 프로젝트 매칭 플랫폼을 활용할 수 있다. 모노레포를 활용한 대규모 프로젝트 관리, 마이크로서비스 아키텍처, 코드 공유가 중요한 프로젝트 등에서 실전 경험을 쌓을 수 있다. 특히 프론트엔드와 백엔드를 함께 관리하거나, 여러 앱을 하나의 저장소에서 관리하는 프로젝트에서 모노레포의 강점을 발휘할 수 있다.