Turborepo 모노레포 가이드 2026 — pnpm 워크스페이스 실전 설정
Turborepo와 pnpm 워크스페이스로 모노레포를 구성하는 실전 가이드. 태스크 파이프라인, 원격 캐싱, 공유 패키지 설정까지 처음부터 단계별로 설명합니다.
모노레포가 필요한 순간
여러 앱이 같은 코드를 복붙하기 시작할 때가 모노레포를 검토할 타이밍입니다. 공유 UI 컴포넌트, 타입 정의, 유틸 함수를 각 레포에서 따로 관리하면 변경 시 모든 곳을 동기화해야 합니다. 모노레포는 이 문제를 하나의 저장소로 해결합니다.
Turborepo는 모노레포의 빌드 복잡도를 캐싱과 병렬 실행으로 관리합니다. 설정이 단순하고 기존 npm/pnpm 워크스페이스 위에 얹는 방식이라 도입 비용이 낮습니다.
프로젝트 구조 설계
일반적인 모노레포 디렉토리 구조
my-monorepo/
├── apps/ # 배포 가능한 애플리케이션
│ ├── web/ # Next.js 웹 앱
│ ├── api/ # Node.js API 서버
│ └── mobile/ # React Native 앱 (선택)
├── packages/ # 공유 패키지
│ ├── ui/ # 공유 React 컴포넌트
│ ├── config/ # 공유 설정 (eslint, tsconfig)
│ └── utils/ # 공유 유틸리티
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
언제 apps/, 언제 packages/로?
| 기준 | apps/ | packages/ |
|---|---|---|
| 독립 실행 가능한가? | ✓ | ✗ |
| 다른 패키지가 의존하는가? | ✗ | ✓ |
| 배포/서빙 대상인가? | ✓ | ✗ |
| 예시 | Next.js 앱, API 서버 | UI 라이브러리, ESLint 설정 |
초기 설정
1. pnpm 워크스페이스 설정
# 프로젝트 루트 초기화
mkdir my-monorepo && cd my-monorepo
pnpm init
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// package.json (루트)
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"turbo": "latest",
"prettier": "^3.0.0"
}
}
2. Turborepo 설치 및 설정
pnpm add turbo -D -w
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // 의존 패키지 먼저 빌드
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "dist/**"]
},
"dev": {
"cache": false, // 개발 서버는 캐시 비활성화
"persistent": true // 프로세스 유지 (watch 모드)
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**"],
"outputs": ["coverage/**"]
},
"type-check": {
"dependsOn": ["^build"]
}
}
}
공유 패키지 구성
공유 TypeScript 설정
// packages/config/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
},
"exclude": ["node_modules"]
}
// packages/config/tsconfig/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"jsx": "preserve"
}
}
// packages/config/package.json
{
"name": "@myapp/config",
"version": "0.0.0",
"private": true,
"exports": {
"./tsconfig": "./tsconfig"
}
}
공유 ESLint 설정
// packages/config/eslint/next.js
const { resolve } = require("node:path");
const project = resolve(process.cwd(), "tsconfig.json");
module.exports = {
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"prettier",
],
parser: "@typescript-eslint/parser",
settings: {
"import/resolver": {
typescript: { project },
},
},
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
},
};
공유 UI 컴포넌트 패키지
// packages/ui/package.json
{
"name": "@myapp/ui",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint . --max-warnings 0",
"type-check": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@myapp/config": "workspace:*",
"@types/react": "^18.0.0",
"typescript": "^5.0.0"
}
}
// packages/ui/src/index.ts
export { Button } from './button';
export { Card } from './card';
export { Input } from './input';
export type { ButtonProps } from './button';
// packages/ui/src/button.tsx
import type { ButtonHTMLAttributes } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export function Button({
variant = 'primary',
size = 'md',
children,
className,
...props
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size} ${className ?? ''}`}
{...props}
>
{children}
</button>
);
}
앱에서 공유 패키지 사용
// apps/web/package.json
{
"name": "@myapp/web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint . --max-warnings 0",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@myapp/ui": "workspace:*", // 공유 UI 패키지
"next": "^14.0.0",
"react": "^18.0.0"
},
"devDependencies": {
"@myapp/config": "workspace:*", // 공유 설정
"typescript": "^5.0.0"
}
}
// apps/web/app/page.tsx
import { Button, Card } from '@myapp/ui'; // 공유 컴포넌트 바로 사용
export default function HomePage() {
return (
<Card>
<h1>Hello from web app</h1>
<Button variant="primary">Click me</Button>
</Card>
);
}
// apps/web/tsconfig.json
{
"extends": "@myapp/config/tsconfig/nextjs",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
태스크 파이프라인 심화
dependsOn 패턴
// turbo.json — dependsOn 패턴 설명
{
"tasks": {
"build": {
// ^build: 의존하는 패키지의 build가 먼저 완료되어야 함
"dependsOn": ["^build"]
},
"test": {
// 같은 패키지의 build가 먼저 완료되어야 함 (^ 없음)
"dependsOn": ["build"]
},
"deploy": {
// 특정 패키지의 태스크를 명시적으로 지정
"dependsOn": ["@myapp/ui#build", "build"]
}
}
}
환경변수와 캐시
// turbo.json — 환경변수 기반 캐시 무효화
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": [
"NODE_ENV",
"NEXT_PUBLIC_API_URL",
"DATABASE_URL"
],
"outputs": [".next/**", "dist/**"]
}
}
}
환경변수가 다르면 캐시가 무효화됩니다. 개발/스테이징/프로덕션 환경을 분리할 때 중요합니다.
원격 캐싱 설정
Vercel 원격 캐싱 (무료)
# Vercel 계정 연결
npx turbo login
npx turbo link
# 이후 turbo 명령 실행 시 자동으로 원격 캐시 사용
turbo build
팀 원격 캐싱 효과:
- A 개발자가 빌드한 결과를 B 개발자가 즉시 재사용
- CI에서 동일 코드 재빌드 없음 → CI 시간 50~90% 단축
자체 원격 캐시 서버
# docker-compose.yml로 간단히 구성 가능
# turborepo-remote-cache 오픈소스 프로젝트 활용
// turbo.json — 자체 캐시 서버
{
"remoteCache": {
"enabled": true,
"apiUrl": "https://your-cache-server.com"
}
}
자주 쓰는 turbo 명령어
# 전체 빌드 (캐시 활용)
turbo build
# 특정 앱만 빌드
turbo build --filter=@myapp/web
# 특정 앱 + 해당 앱의 의존 패키지 빌드
turbo build --filter=@myapp/web...
# 변경된 파일이 있는 패키지만 (git diff 기반)
turbo build --filter=[HEAD^1]
# 캐시 무시하고 전체 재빌드
turbo build --force
# 병렬 실행 제한 (CI 환경에서 리소스 제어)
turbo build --concurrency=4
# 태스크 실행 그래프 시각화
turbo build --graph
# 특정 태스크의 캐시 삭제
turbo build --force --filter=@myapp/web
CI/CD 설정
GitHub Actions 예시
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
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: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Vercel 원격 캐싱 활성화
- name: Build
run: pnpm turbo build lint type-check
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Turbo 캐시로 CI 시간 단축
원격 캐시 적용 전후 비교 (실제 규모에 따라 다름):
| 구분 | 캐시 없음 | 원격 캐시 적중 |
|---|---|---|
| 패키지 빌드 | 4분 | 15초 |
| 린트 | 2분 | 5초 |
| 타입체크 | 3분 | 10초 |
| 테스트 | 5분 | 30초 |
| 총 CI 시간 | 약 14분 | 약 1분 |
자주 묻는 질문 (FAQ)
Q. Turborepo가 Nx보다 나은 점은 무엇인가요?
설정 복잡도가 낮고, 기존 pnpm/npm 워크스페이스 위에 얹는 방식이라 점진적 도입이 쉽습니다. Nx는 Angular, Java 등 다양한 생태계를 지원하고 기능이 풍부하지만 러닝커브가 큽니다. JavaScript/TypeScript 중심 스타트업이나 소규모 팀에는 Turborepo가 실용적입니다.
Q. 모노레포로 전환하는 비용은 어느 정도인가요?
기존 멀티레포 앱 2~3개를 모노레포로 통합하는 작업은 보통 며칠 ~ 2주 내로 가능합니다. 공유 코드 분리, CI/CD 재구성, 팀원 교육이 주요 비용입니다. Turborepo는 기존 스크립트를 그대로 재사용하므로 앱 자체의 코드 변경은 최소화됩니다.
Q. 원격 캐싱은 무료인가요?
Vercel 계정으로 연결하는 Turborepo 원격 캐시는 개인·소규모 팀에 무료입니다. 사용량이 많아지면 Vercel 유료 플랜이 필요할 수 있습니다. 오픈소스 turborepo-remote-cache 프로젝트로 자체 서버를 구축하면 완전 무료로 운영할 수 있습니다.
Q. 모노레포에서 특정 앱만 배포하는 방법은?
turbo build --filter=@myapp/web으로 특정 앱만 빌드한 후 해당 결과물만 배포합니다. Vercel이나 Netlify는 모노레포 루트 디렉토리와 빌드 명령을 앱별로 지정할 수 있습니다. 변경된 앱만 선택적으로 배포하는 --filter=[HEAD^1] 패턴도 자주 활용됩니다.
핵심 정리
| 항목 | 요점 |
|---|---|
| 기본 구조 | apps/(실행 앱) + packages/(공유 패키지) |
| 패키지 참조 | "@myapp/ui": "workspace:*" |
| 태스크 파이프라인 | "dependsOn": ["^build"] — 의존 순서 자동 처리 |
| 캐싱 | 입력/출력 해시로 변경 없으면 재실행 없음 |
| 원격 캐시 | Vercel 연결로 팀 전체 캐시 공유 |
| 필터 | --filter=@myapp/web... 로 특정 패키지 타겟팅 |
모노레포는 처음 설정이 번거롭지만, 공유 코드가 생기는 순간부터 복붙 지옥에서 해방됩니다. Turborepo는 그 진입 비용을 최소화하는 도구입니다.
관련 글: pnpm vs npm 패키지 매니저 비교 · GitHub Actions CI/CD 가이드 · TypeScript 패턴 실전 가이드
관련 글
pnpm vs npm vs yarn 완전 비교 2026 — 개발자를 위한 패키지 매니저 선택 가이드
pnpm, npm, yarn의 속도·디스크 효율·모노레포 지원을 2026년 기준으로 비교합니다. 프로젝트 규모와 팀 상황에 맞는 패키지 매니저 선택 기준과 pnpm 마이그레이션 방법을 정리했습니다.
AWS 비용 최적화 완전 가이드 2026 — EC2, S3, RDS 비용 절감 실전
AWS 비용이 예상보다 많이 나오는 개발자를 위한 최적화 가이드. EC2 Reserved Instance vs Savings Plans, S3 스토리지 클래스, RDS 비용 절감, 무료 티어 한도, 비용 모니터링 설정까지 실전 방법을 정리했습니다.
Git 고급 명령어 실무 가이드 2026 — rebase, stash, cherry-pick 완전 정리
개발자가 실무에서 꼭 알아야 할 Git 고급 명령어를 정리했습니다. rebase vs merge, interactive rebase, stash, cherry-pick, bisect, reflog로 실수 복구까지 실전 예시와 함께 설명합니다.