오늘도 공부
Turborepo로 Next.js 16 모노레포 구축하기 본문
목차
- 모노레포란 무엇인가?
- 왜 Turborepo인가?
- 실전: Turborepo 프로젝트 구축
- 여러 Next.js 앱 동시 관리하기
- 공유 UI 컴포넌트 라이브러리 만들기
- Shadcn UI와 통합하기
- 성능 최적화 팁
- 실제 프로덕션 배포 전략
모노레포란 무엇인가?
여러분이 다음과 같은 프로젝트를 운영한다고 상상해보세요:
- 고객용 웹 앱
- 관리자 대시보드
- 모바일 앱용 API
- 마케팅 랜딩 페이지
전통적인 방식(Polyrepo)에서는 각 프로젝트마다 별도의 Git 저장소를 만듭니다. 하지만 공통 UI 컴포넌트를 수정하려면 어떻게 될까요?
❌ 기존 방식 (Polyrepo)
1. UI 라이브러리 저장소에서 Button 컴포넌트 수정
2. npm에 새 버전 배포 (v1.2.3)
3. 웹 앱 저장소에서 package.json 버전 업데이트
4. 관리자 대시보드 저장소에서도 버전 업데이트
5. 각각 테스트, 배포...
🎉 모노레포 방식 (Turborepo)
1. 공유 UI 패키지에서 Button 컴포넌트 수정
2. 끝! 모든 앱에 즉시 반영됨
**모노레포(Monorepo)**는 여러 프로젝트를 하나의 저장소에서 관리하는 코드 구성 방식입니다. Google, Microsoft, Meta 같은 거대 기업들이 이미 수년간 사용해온 검증된 방법입니다.
모노레포의 장점
✅ 원자적 커밋(Atomic Commits): 여러 프로젝트에 걸친 변경사항을 하나의 커밋으로 처리
✅ 의존성 지옥 탈출: 버전 충돌과 의존성 문제 해결
✅ 코드 재사용: 공통 로직을 쉽게 공유
✅ 일관된 개발 환경: 모든 프로젝트에서 동일한 ESLint, TypeScript 설정 사용
✅ 전체 시스템 가시성: 팀 전체가 모든 코드에 접근 가능
모노레포 ≠ 모놀리식
⚠️ 주의: 모노레포는 코드 구성 방식일 뿐, 모놀리식 아키텍처와는 다릅니다!
- 모노레포: 여러 독립적인 프로젝트를 한 저장소에서 관리
- 모놀리식: 모든 기능이 하나로 결합된 단일 애플리케이션
왜 Turborepo인가?
2025년 현재, 모노레포 도구 시장에는 여러 선택지가 있습니다:
도구 장점 단점
| Turborepo | 설정 간단, Next.js와 완벽 통합, 빠른 빌드 | 복잡한 프로젝트에서는 커스터마이징 제한적 |
| Nx | 강력한 기능, 엔터프라이즈급 | 학습 곡선 높음, 설정 복잡 |
| Lerna | 레거시 지원 우수 | 개발 속도 느림 |
Turborepo의 핵심 강점
1. 🚀 속도 (40-85% 빌드 시간 단축)
Turborepo는 지능적인 캐싱과 병렬 처리를 통해 빌드 시간을 40-85% 단축합니다.
# 첫 번째 빌드
$ turbo build
>>> Building...
✓ Built in 45s
# 아무것도 변경하지 않고 다시 빌드
$ turbo build
>>> FULL TURBO (cached)
✓ Built in 100ms # 🎉 450배 빠름!
어떻게 가능한가?
Turborepo는 빌드마다 해시를 생성하고, 캐시에서 해시를 찾지 못하면 빌드를 실행한 후 결과를 저장합니다. 다음에 동일한 소스로 빌드할 때는 빌드 과정을 건너뛰고 캐시에서 결과를 복원합니다.
2. 🔄 병렬 실행
Turborepo는 작업을 병렬로 스케줄링하여 CPU 코어를 최대한 활용해 빌드를 최적화합니다.
전통적인 방식:
build ui → build web → build admin → build api
(순차 실행, 총 4분)
Turborepo:
build ui
↓
build web + build admin + build api
(병렬 실행, 총 1.5분)
3. 💾 원격 캐싱
Turborepo는 원격 캐싱을 통해 팀 전체와 CI/CD 파이프라인에서 캐시를 공유할 수 있어, CI가 동일한 작업을 두 번 수행할 필요가 없습니다.
# 팀원 A가 빌드
$ turbo build
✓ Built and cached
# 팀원 B가 같은 코드로 빌드
$ turbo build
>>> Fetching from remote cache...
✓ Built in 500ms # 원격 캐시에서 복원!
4. 🔍 증분 빌드
변경된 패키지만 재빌드합니다. UI 컴포넌트를 수정했다면, 그것을 사용하는 앱만 다시 빌드됩니다.
실전: Turborepo 프로젝트 구축
1단계: 프로젝트 생성
pnpm을 사용하여 새로운 Turborepo 프로젝트를 생성합니다.
# pnpm 사용 (권장)
pnpm create turbo@latest
# 또는 npx
npx create-turbo@latest
# 프로젝트 이름 입력
>>> What is your project named? my-awesome-project
>>> Which package manager? pnpm
2단계: 폴더 구조 이해하기
생성된 모노레포는 apps(배포 가능한 애플리케이션)와 packages(공유 패키지)로 나뉩니다.
my-awesome-project/
├── apps/ # 배포 가능한 애플리케이션들
│ ├── web/ # 고객용 웹사이트 (포트 3000)
│ │ ├── app/
│ │ ├── package.json
│ │ └── next.config.js
│ ├── admin/ # 관리자 대시보드 (포트 3001)
│ │ ├── app/
│ │ ├── package.json
│ │ └── next.config.js
│ └── docs/ # 문서 사이트 (포트 3002)
│ ├── app/
│ ├── package.json
│ └── next.config.js
│
├── packages/ # 공유 패키지들
│ ├── ui/ # 공유 UI 컴포넌트
│ │ ├── src/
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── database/ # 공유 데이터베이스 로직
│ │ ├── src/
│ │ └── package.json
│ ├── auth/ # 공유 인증 로직
│ │ ├── src/
│ │ └── package.json
│ ├── tsconfig/ # 공유 TypeScript 설정
│ │ ├── base.json
│ │ ├── nextjs.json
│ │ └── react-library.json
│ └── eslint-config/ # 공유 ESLint 설정
│ └── index.js
│
├── turbo.json # Turborepo 설정
├── package.json # 루트 package.json
├── pnpm-workspace.yaml # pnpm 워크스페이스 설정
└── .gitignore
3단계: turbo.json 설정
turbo.json 파일은 작업 파이프라인을 정의하며, 각 작업의 의존성과 캐싱 방법을 지정합니다.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"type-check": {
"dependsOn": ["^type-check"]
}
}
}
설정 상세 설명:
- dependsOn: ["^build"]: 의존하는 패키지를 먼저 빌드
- outputs: 캐싱할 출력 디렉토리
- cache: false: dev 서버는 캐싱하지 않음
- persistent: true: dev 서버를 백그라운드에서 계속 실행
4단계: 루트 package.json 설정
{
"name": "my-awesome-project",
"private": true,
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"type-check": "turbo type-check",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.3.0",
"typescript": "^5.6.0"
},
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18.0.0"
}
}
5단계: pnpm 워크스페이스 설정
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
여러 Next.js 앱 동시 관리하기
Turborepo를 사용하면 하나의 저장소에서 여러 Next.js 앱을 효율적으로 관리할 수 있습니다.
실전 예제: 이커머스 플랫폼
my-ecommerce/
├── apps/
│ ├── storefront/ # 고객용 쇼핑몰 (포트 3000)
│ ├── admin/ # 판매자 관리 시스템 (포트 3001)
│ ├── mobile-api/ # 모바일 앱용 API (포트 3002)
│ └── blog/ # 마케팅 블로그 (포트 3003)
└── packages/
├── ui/ # 공통 디자인 시스템
├── database/ # Prisma 스키마 및 클라이언트
├── auth/ # NextAuth 설정
└── utils/ # 공통 유틸리티 함수
각 앱 설정하기
apps/storefront/package.json
{
"name": "storefront",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start -p 3000",
"lint": "next lint"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/database": "workspace:*",
"@repo/auth": "workspace:*",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@repo/eslint-config": "workspace:*",
"typescript": "^5.6.0"
}
}
apps/admin/package.json
{
"name": "admin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "next lint"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/database": "workspace:*",
"@repo/auth": "workspace:*",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Next.js 16 특화 설정
apps/storefront/next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
// 공유 패키지를 트랜스파일
transpilePackages: ['@repo/ui', '@repo/auth'],
// Next.js 16의 새로운 기능들
experimental: {
// Turbopack 파일시스템 캐싱 (Next.js 16 베타 기능)
turbopack: {
resolveAlias: {
'@': './src',
},
},
// 부분적 사전 렌더링
ppr: true,
// React 19 기능 활성화
reactCompiler: true,
},
// 이미지 최적화
images: {
domains: ['images.example.com'],
},
}
모든 앱 동시 실행하기
# 모든 앱을 동시에 dev 모드로 실행
$ turbo dev
# 출력:
# • storefront:dev: ready - started server on 0.0.0.0:3000
# • admin:dev: ready - started server on 0.0.0.0:3001
# • mobile-api:dev: ready - started server on 0.0.0.0:3002
# • blog:dev: ready - started server on 0.0.0.0:3003
필터를 사용한 선택적 실행
필터 플래그를 사용하여 특정 스크립트만 실행할 수 있습니다.
# storefront 앱만 실행
$ turbo dev --filter=storefront
# storefront와 admin만 실행
$ turbo dev --filter=storefront --filter=admin
# storefront와 그 의존성만 실행
$ turbo dev --filter=storefront...
# 변경된 파일이 있는 앱만 빌드
$ turbo build --filter=[HEAD^1]
# 특정 패키지에 의존하는 모든 앱 빌드
$ turbo build --filter=...@repo/ui
공유 UI 컴포넌트 라이브러리 만들기
UI 패키지 구조
packages/ui/
├── src/
│ ├── components/
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ └── modal.tsx
│ ├── hooks/
│ │ ├── use-toast.ts
│ │ └── use-media-query.ts
│ ├── utils/
│ │ └── cn.ts
│ └── index.ts
├── package.json
├── tsconfig.json
└── tailwind.config.ts
packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": "./src/components/button.tsx",
"./card": "./src/components/card.tsx",
"./input": "./src/components/input.tsx",
"./hooks": "./src/hooks/index.ts",
"./utils": "./src/utils/index.ts",
"./styles": "./src/styles/globals.css"
},
"scripts": {
"lint": "eslint . --max-warnings 0",
"type-check": "tsc --noEmit"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@repo/eslint-config": "workspace:*",
"@types/react": "^19.0.0",
"typescript": "^5.6.0",
"tailwindcss": "^3.4.0"
}
}
버튼 컴포넌트 예제
packages/ui/src/components/button.tsx
import * as React from 'react';
import { cn } from '../utils/cn';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({
className,
variant = 'primary',
size = 'md',
isLoading = false,
disabled,
children,
...props
}, ref) => {
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-700',
outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50',
ghost: 'text-blue-600 hover:bg-blue-50',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
ref={ref}
className={cn(
'rounded-lg font-medium transition-colors',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
variants[variant],
sizes[size],
className
)}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
로딩 중...
</span>
) : (
children
)}
</button>
);
}
);
Button.displayName = 'Button';
앱에서 공유 컴포넌트 사용하기
apps/storefront/app/page.tsx
import { Button } from '@repo/ui/button';
import { Card } from '@repo/ui/card';
import { useToast } from '@repo/ui/hooks';
export default function HomePage() {
const { toast } = useToast();
const handleClick = () => {
toast({
title: '성공!',
description: '버튼이 클릭되었습니다.',
});
};
return (
<div className="container mx-auto p-8">
<Card>
<h1 className="text-3xl font-bold mb-4">
환영합니다!
</h1>
<p className="text-gray-600 mb-6">
Turborepo와 Next.js 16으로 만든 모노레포 앱입니다.
</p>
<div className="flex gap-3">
<Button onClick={handleClick}>
기본 버튼
</Button>
<Button variant="secondary">
보조 버튼
</Button>
<Button variant="outline">
아웃라인 버튼
</Button>
<Button isLoading>
로딩 버튼
</Button>
</div>
</Card>
</div>
);
}
apps/admin/app/dashboard/page.tsx
import { Button } from '@repo/ui/button';
import { Card } from '@repo/ui/card';
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
<Card>
<h3 className="font-semibold mb-2">총 매출</h3>
<p className="text-3xl font-bold">₩12,345,678</p>
<Button size="sm" className="mt-4">
상세 보기
</Button>
</Card>
<Card>
<h3 className="font-semibold mb-2">주문 수</h3>
<p className="text-3xl font-bold">1,234</p>
<Button size="sm" className="mt-4">
주문 관리
</Button>
</Card>
<Card>
<h3 className="font-semibold mb-2">고객 수</h3>
<p className="text-3xl font-bold">5,678</p>
<Button size="sm" className="mt-4">
고객 관리
</Button>
</Card>
</div>
);
}
타입 안정성
TypeScript 타입이 모든 앱에서 자동으로 공유됩니다:
// 타입 자동 완성과 검증
<Button
variant="primary" // ✅ 자동 완성
size="md" // ✅ 자동 완성
onClick={...} // ✅ 타입 검증
invalidProp="x" // ❌ 타입 오류!
/>
Shadcn UI와 통합하기
Shadcn/ui는 Tailwind CSS로 만든 아름다운 디자인의 오픈소스 컴포넌트 세트로, Turborepo 모노레포에서 사용할 수 있습니다.
Shadcn UI 설치하기
# 프로젝트 루트에서 실행
$ npx shadcn@latest init
# 모노레포 옵션 선택
>>> Would you like to use a monorepo? Yes
>>> Which workspace would you like to use? packages/ui
UI 패키지 구조 (Shadcn 포함)
packages/ui/
├── src/
│ ├── components/
│ │ ├── ui/ # Shadcn 컴포넌트들
│ │ │ ├── button.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ └── toast.tsx
│ │ └── custom/ # 커스텀 컴포넌트들
│ │ ├── product-card.tsx
│ │ └── user-avatar.tsx
│ ├── lib/
│ │ └── utils.ts
│ └── index.ts
├── components.json # Shadcn 설정
├── package.json
└── tailwind.config.ts
Shadcn 컴포넌트 추가하기
# UI 패키지에 컴포넌트 추가
$ cd packages/ui
$ npx shadcn@latest add button dialog toast
# 또는 루트에서 실행
$ npx shadcn@latest add button dialog toast --path packages/ui
components.json 설정
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "./src/components",
"utils": "./src/lib/utils"
}
}
Tailwind 설정 공유하기
packages/ui/tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;
앱에서 Shadcn 컴포넌트 사용하기
apps/storefront/app/products/page.tsx
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Toast,
ToastProvider
} from '@repo/ui';
import { useState } from 'react';
export default function ProductsPage() {
const [open, setOpen] = useState(false);
return (
<ToastProvider>
<div className="container mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">상품 목록</h1>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
상품 추가하기
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>새 상품 추가</DialogTitle>
<DialogDescription>
새로운 상품 정보를 입력하세요.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="name" className="text-right">
상품명
</label>
<input
id="name"
className="col-span-3"
placeholder="상품명을 입력하세요"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="price" className="text-right">
가격
</label>
<input
id="price"
type="number"
className="col-span-3"
placeholder="가격을 입력하세요"
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
취소
</Button>
<Button onClick={() => {
// 상품 저장 로직
setOpen(false);
}}>
저장
</Button>
</div>
</DialogContent>
</Dialog>
{/* 상품 목록 */}
<div className="grid grid-cols-3 gap-6 mt-8">
{/* 상품 카드들 */}
</div>
</div>
</ToastProvider>
);
}
커스텀 컴포넌트 만들기
Shadcn 컴포넌트를 기반으로 도메인 특화 컴포넌트를 만들 수 있습니다:
packages/ui/src/components/custom/product-card.tsx
import { Button } from '../ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../ui/card';
interface ProductCardProps {
title: string;
price: number;
image: string;
onAddToCart: () => void;
}
export function ProductCard({ title, price, image, onAddToCart }: ProductCardProps) {
return (
<Card className="overflow-hidden">
<img
src={image}
alt={title}
className="w-full h-48 object-cover"
/>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">
₩{price.toLocaleString()}
</p>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={onAddToCart}
>
장바구니에 추가
</Button>
</CardFooter>
</Card>
);
}
성능 최적화 팁
1. 의존성 그래프 최적화
Turborepo는 의존성 그래프를 기반으로 최적화된 병렬 실행을 수행합니다.
// turbo.json
{
"tasks": {
"build": {
// ^build: 의존하는 패키지의 build가 먼저 완료되어야 함
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
// build와 lint가 먼저 완료되어야 함
"dependsOn": ["build", "lint"],
"outputs": ["coverage/**"]
},
"lint": {
// 의존성 없음, 즉시 실행 가능
"outputs": []
}
}
}
2. 캐싱 전략
Turborepo의 캐싱은 매우 효과적입니다. 실제 프로젝트에서 측정된 성능 지표를 보면 그 효과를 확인할 수 있습니다.
# 첫 번째 빌드
$ turbo build
✓ Built in 2m 15s
# 변경 없이 재빌드
$ turbo build
>>> FULL TURBO
✓ Built in 847ms
# UI 패키지만 수정 후 재빌드
$ turbo build
>>> Cache miss: @repo/ui
>>> Cache hit: @repo/database, @repo/auth
✓ Built in 12s
캐싱 전략 최적화:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
// 환경 변수가 변경되면 캐시 무효화
"env": ["NODE_ENV", "API_URL"]
},
"test": {
// 테스트 결과 캐싱
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
// 빠른 명령은 캐싱 불필요
"cache": false
}
}
}
3. 원격 캐싱 설정
원격 캐싱을 사용하면 팀원들과 CI/CD 파이프라인 간에 빌드 캐시를 공유할 수 있습니다.
# Vercel 계정으로 로그인
$ npx turbo login
# 프로젝트 연결
$ npx turbo link
# 이제 원격 캐시 사용!
$ turbo build
>>> Remote caching enabled
>>> Downloading cache from remote...
✓ Built in 2s
4. 빌드 최적화
Next.js 16 설정 최적화:
// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
transpilePackages: ['@repo/ui', '@repo/database'],
// 실험적 기능
experimental: {
// Turbopack 캐싱 (Next.js 16)
turbopack: true,
// 부분 사전 렌더링
ppr: true,
// React 컴파일러
reactCompiler: true,
},
// 번들 분석
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.alias = {
...config.resolve.alias,
'@': __dirname,
};
}
return config;
},
// 압축 최적화
compress: true,
// 이미지 최적화
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
},
};
5. 패키지 크기 최적화
불필요한 의존성 제거:
# 사용하지 않는 패키지 찾기
$ npx depcheck
# 중복된 패키지 찾기
$ pnpm dedupe
# 번들 크기 분석
$ npx @next/bundle-analyzer
6. 타입스크립트 최적화
packages/tsconfig/base.json
{
"compilerOptions": {
"incremental": true,
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo",
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
실제 프로덕션 배포 전략
Vercel 배포 (권장)
Turborepo는 Vercel에서 개발한 도구로, Vercel 플랫폼과의 통합이 매우 원활합니다.
1. 각 앱별 vercel.json 설정
// apps/storefront/vercel.json
{
"buildCommand": "cd ../.. && turbo build --filter=storefront",
"outputDirectory": ".next",
"framework": "nextjs",
"installCommand": "pnpm install"
}
2. GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
# Turborepo 원격 캐싱 설정
- name: Setup Turborepo Remote Cache
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
run: |
echo "TURBO_TOKEN=$TURBO_TOKEN" >> $GITHUB_ENV
echo "TURBO_TEAM=$TURBO_TEAM" >> $GITHUB_ENV
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm turbo build
- name: Test
run: pnpm turbo test
- name: Lint
run: pnpm turbo lint
# Vercel 배포
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: ./apps/storefront
Docker 컨테이너 배포
Turborepo는 프루닝(pruning)을 통해 특정 타겟 빌드에 필요한 것만 포함된 희소 모노레포 하위 집합을 생성할 수 있습니다.
Dockerfile (Multi-stage build)
# Base image
FROM node:20-alpine AS base
RUN corepack enable pnpm
# Pruned stage - 필요한 것만 추출
FROM base AS pruner
WORKDIR /app
RUN pnpm add -g turbo
COPY . .
RUN turbo prune --scope=storefront --docker
# Dependencies stage
FROM base AS installer
WORKDIR /app
# 먼저 의존성만 설치 (캐싱 최적화)
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
# 소스 코드 복사 및 빌드
COPY --from=pruner /app/out/full/ .
COPY turbo.json turbo.json
RUN pnpm turbo build --filter=storefront
# Runner stage - 최종 실행 이미지
FROM base AS runner
WORKDIR /app
# 프로덕션 전용 사용자 생성
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
# 빌드 결과물만 복사
COPY --from=installer --chown=nextjs:nodejs /app/apps/storefront/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/storefront/.next/static ./apps/storefront/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/storefront/public ./apps/storefront/public
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "apps/storefront/server.js"]
docker-compose.yml
version: '3.8'
services:
storefront:
build:
context: .
dockerfile: apps/storefront/Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
depends_on:
- database
admin:
build:
context: .
dockerfile: apps/admin/Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
depends_on:
- database
database:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
환경 변수 관리
루트 .env.example
# Database
DATABASE_URL="postgresql://admin:password@localhost:5432/myapp"
# Auth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-key"
# API Keys
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Analytics
GOOGLE_ANALYTICS_ID="G-XXXXXXXXXX"
앱별 환경 변수:
# apps/storefront/.env.local
NEXT_PUBLIC_API_URL="http://localhost:3002"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
# apps/admin/.env.local
NEXT_PUBLIC_API_URL="http://localhost:3002"
NEXT_PUBLIC_SITE_URL="http://localhost:3001"
실전 팁과 트러블슈팅
1. 패키지 버전 불일치 문제
일관된 패키지 관리자 선택이 중요하며, workspace 프로토콜을 사용하여 로컬 패키지를 참조하면 변경사항이 즉시 반영됩니다.
// 올바른 방법: workspace 프로토콜 사용
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/database": "workspace:*"
}
}
// 잘못된 방법
{
"dependencies": {
"@repo/ui": "file:../../packages/ui" // ❌
}
}
2. TypeScript 경로 문제
// apps/storefront/tsconfig.json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@repo/ui": ["../../packages/ui/src"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
3. 개발 서버 포트 충돌
# 각 앱에 고유한 포트 할당
# apps/storefront/package.json
"dev": "next dev -p 3000"
# apps/admin/package.json
"dev": "next dev -p 3001"
# apps/docs/package.json
"dev": "next dev -p 3002"
4. 빌드 시간 개선
# 변경된 패키지만 빌드
$ turbo build --filter=[HEAD^1]
# 병렬 작업 수 제한 (메모리 부족 시)
$ turbo build --concurrency=2
# 특정 패키지만 빌드
$ turbo build --filter=storefront...
5. 캐시 문제 해결
# 로컬 캐시 삭제
$ rm -rf .turbo
# 모든 node_modules 삭제 후 재설치
$ rm -rf node_modules apps/*/node_modules packages/*/node_modules
$ pnpm install
# 빌드 아티팩트 삭제
$ turbo clean
$ pnpm install
$ turbo build
마무리
핵심 정리
✅ Turborepo는 모노레포 관리의 게임 체인저
- 40-85% 빌드 시간 단축
- 지능적인 캐싱과 병렬 처리
- Next.js와의 완벽한 통합
✅ 여러 Next.js 앱을 하나의 저장소에서 관리
- 코드 재사용성 극대화
- 일관된 개발 환경
- 독립적인 배포 가능
✅ Shadcn UI로 디자인 시스템 구축
- 아름답고 접근성 높은 컴포넌트
- Tailwind CSS 기반
- 커스터마이징 용이
✅ 프로덕션 레디
- Vercel 배포 최적화
- Docker 컨테이너화
- CI/CD 파이프라인 구축
다음 단계
- 프로젝트 시작하기
- pnpm create turbo@latest my-project cd my-project pnpm dev
- 문서 살펴보기
- 커뮤니티 참여하기
추가 리소스
- Next-forge: 프로덕션 급 Turborepo 템플릿
- Turborepo 예제 저장소
- 모노레포 베스트 프랙티스
행복한 코딩 되세요! 🚀
