이 글은 누구를 위한 것인가
- 여러 Next.js 앱에서 같은 컴포넌트를 복사해 쓰는 팀
- 모노레포를 도입했지만 빌드가 느리거나 태스크 순서가 맞지 않는 팀
- Turborepo 공식 문서를 봤지만 실제 프로젝트에 어떻게 적용할지 모르는 개발자
모노레포가 필요한 신호
- 여러 앱이 같은 컴포넌트, 타입, 유틸리티를 복사해서 사용
- 공통 코드를 수정했을 때 모든 앱에 반영하는 것이 번거롭다
- 여러 패키지를 별도 저장소로 분리했지만 동시 변경 시 버저닝이 복잡하다
1. 초기 설정
# pnpm 필수 (npm workspace도 가능하지만 pnpm 권장)
npx create-turbo@latest my-monorepo
cd my-monorepo
디렉토리 구조
my-monorepo/
├── apps/
│ ├── web/ # Next.js 메인 앱
│ ├── admin/ # Next.js 관리자 앱
│ └── docs/ # 문서 사이트
├── packages/
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── config/ # 공유 설정 (ESLint, TypeScript, Tailwind)
│ ├── types/ # 공유 타입
│ └── utils/ # 공유 유틸리티
├── package.json # 루트 package.json
├── pnpm-workspace.yaml
└── turbo.json
pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
2. turbo.json 파이프라인 설정
Turborepo의 핵심은 태스크 간 의존성과 캐싱을 turbo.json에 정의하는 것이다.
{
"$schema": "https://turbo.build/schema.json",
"globalEnv": ["NODE_ENV", "VERCEL_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"], // 의존하는 패키지의 build가 먼저 실행됨
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**"],
"outputs": ["coverage/**"]
},
"lint": {
"dependsOn": [],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "*.config.*"]
},
"dev": {
"dependsOn": ["^build"],
"persistent": true, // 장시간 실행 태스크
"cache": false // dev 서버는 캐시 안 함
},
"type-check": {
"dependsOn": ["^build"]
}
}
}
^build 의미: 이 태스크를 실행하기 전에 의존하는 모든 패키지의 build를 먼저 실행.
packages/ui를 의존하는 apps/web에서 build를 실행하면:
packages/ui의build실행apps/web의build실행
3. 공유 패키지 설정
packages/ui - 공유 컴포넌트
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": {
"types": "./src/button.tsx",
"default": "./src/button.tsx"
},
"./card": "./src/card.tsx"
},
"scripts": {
"lint": "eslint src/",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@repo/config": "workspace:*",
"typescript": "^5",
"react": "^18"
},
"peerDependencies": {
"react": "^18"
}
}
// packages/ui/src/button.tsx
import { type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
packages/config - 공유 설정
// packages/config/eslint/index.js
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['next/core-web-vitals', 'prettier'],
rules: {
'no-unused-vars': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
};
// packages/config/typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2017",
"lib": ["es2017"],
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "preserve",
"resolveJsonModule": true,
"incremental": true
},
"exclude": ["node_modules"]
}
앱에서 패키지 사용
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}
// apps/web/app/page.tsx
import { Button } from '@repo/ui/button';
import { formatDate } from '@repo/utils';
export default function Page() {
return (
<Button variant="primary">
{formatDate(new Date())}
</Button>
);
}
4. Tailwind CSS 공유 설정
// packages/config/tailwind/index.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [], // 각 앱에서 오버라이드
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
}
},
fontFamily: {
sans: ['Pretendard Variable', 'system-ui', 'sans-serif'],
},
},
},
};
// apps/web/tailwind.config.js
const baseConfig = require('@repo/config/tailwind');
module.exports = {
...baseConfig,
content: [
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.{ts,tsx}', // UI 패키지 포함
],
};
5. Remote Cache로 CI 속도 최적화
Turborepo Remote Cache는 로컬 캐시를 팀 전체와 CI가 공유한다. 한 번 빌드한 결과를 팀원이나 CI가 재사용할 수 있다.
Vercel Remote Cache (무료)
npx turbo login
npx turbo link
.turbo/config.json에 팀 정보가 저장되고, 이후 turbo build가 원격 캐시를 활용한다.
GitHub Actions 설정
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build, lint, test
run: pnpm turbo build lint test type-check
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # Remote Cache 토큰
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
6. 선택적 빌드: 변경된 패키지만
# 변경된 패키지와 그에 의존하는 패키지만 빌드
pnpm turbo build --filter='...[origin/main]'
# 특정 앱만 빌드
pnpm turbo build --filter=web
# 특정 패키지와 의존성 포함
pnpm turbo build --filter=@repo/ui...
PR에서 변경된 부분만 빌드·테스트하면 CI 시간을 크게 줄일 수 있다.
맺으며
Turborepo 모노레포의 핵심 가치는 두 가지다: 공통 코드의 단일 진실, 빌드 캐싱으로 CI 속도 향상.
공통 UI 컴포넌트를 packages/ui로 분리하는 것부터 시작한다. 처음에 완벽한 구조를 만들려 하지 않아도 된다 — 모노레포의 장점은 나중에 패키지를 분리하고 이동하는 것이 쉽다는 것이다. turbo.json의 dependsOn을 올바르게 설정하는 것이 가장 중요한 첫 번째 단계다.