9.4 모노레포 설정 — npm workspaces와 Turborepo
모노레포란?
**모노레포(Monorepo)**는 여러 패키지/앱을 하나의 git 저장소에서 관리하는 방식입니다.
my-monorepo/
├── apps/
│ ├── web/ # Next.js 웹앱
│ └── mobile/ # React Native 앱
├── packages/
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── types/ # 공유 타입 정의
│ └── utils/ # 공유 유틸리티 함수
├── package.json # 루트 (workspaces 선언)
└── turbo.json # Turborepo 설정
장점:
- 코드 공유 쉬움
- 일관된 개발 환경
- 원자적 변경 (여러 패키지를 한 번에 커밋)
- 의존성 중복 제거
npm workspaces 설정
루트 package.json
// package.json (루트)
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"test": "turbo test",
"typecheck": "turbo typecheck"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
}
}
패키지 설치
# 루트에 설치 (모든 workspace에서 사용)
npm install --save-dev typescript -w
# 특정 workspace에 설치
npm install react -w apps/web
npm install --save-dev @types/react -w apps/web
# 내부 패키지 참조
npm install @myapp/ui -w apps/web
npm install @myapp/types -w packages/utils
공유 타입 패키지 구성
packages/types/
packages/types/
├── src/
│ ├── user.ts
│ ├── product.ts
│ ├── api.ts
│ └── index.ts
├── package.json
└── tsconfig.json
// packages/types/package.json
{
"name": "@myapp/types",
"version": "0.0.1",
"main": "./src/index.ts", // 개발 시 TS 소스 직접 참조
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}
// packages/types/src/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
export type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
export type UpdateUserInput = Partial<CreateUserInput>;
// packages/types/src/api.ts
export interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
hasNext: boolean;
}
export type ApiError = {
code: string;
message: string;
details?: Record<string, string[]>;
};
// packages/types/src/index.ts
export * from './user';
export * from './product';
export * from './api';
공유 UI 컴포넌트 패키지
packages/ui/
// packages/ui/package.json
{
"name": "@myapp/ui",
"version": "0.0.1",
"main": "./src/index.ts",
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
}
// packages/ui/src/Button.tsx
import React from 'react';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
tsconfig 공유 설정
packages/tsconfig/ (공유 tsconfig)
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
// packages/tsconfig/react.json
{
"extends": "./base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"noEmit": true
}
}
// packages/tsconfig/node.json
{
"extends": "./base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
앱에서 공유 tsconfig 사용
// apps/web/tsconfig.json
{
"extends": "@myapp/tsconfig/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "next-env.d.ts"]
}
Turborepo 설정
Turborepo는 모노레포 빌드 캐싱과 태스크 오케스트레이션 도구입니다.
설치
npm install --save-dev turbo
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // 의존하는 패키지 먼저 빌드
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false, // dev는 캐시 안 함
"persistent": true // 백그라운드에서 계속 실행
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
},
"lint": {
"outputs": []
}
}
}
Turborepo 명령어
# 전체 빌드 (캐시 활용)
turbo build
# 특정 패키지만 빌드
turbo build --filter=@myapp/web
# 변경된 패키지만 빌드 (git diff 기반)
turbo build --filter=...[HEAD^1]
# 캐시 무시하고 강제 실행
turbo build --force
# 원격 캐시 활성화 (Vercel)
turbo login
turbo link
TypeScript Project References + 모노레포
더 세밀한 증분 빌드를 위해 Project References를 함께 사용합니다.
// packages/types/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// apps/web/tsconfig.json
{
"compilerOptions": { },
"references": [
{ "path": "../../packages/types" },
{ "path": "../../packages/ui" }
]
}
# 프로젝트 참조를 포함한 전체 빌드
tsc --build apps/web
실전 모노레포 예시
my-saas/
├── apps/
│ ├── web/ # Next.js 앱
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── api/ # Express/NestJS API
│ ├── src/
│ ├── package.json
│ └── tsconfig.json
├── packages/
│ ├── database/ # Prisma 스키마 + 클라이언트
│ │ ├── prisma/schema.prisma
│ │ ├── src/
│ │ └── package.json
│ ├── types/ # 공유 타입
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── utils/ # 공유 유틸리티
│ └── tsconfig/ # 공유 tsconfig
├── package.json
└── turbo.json
// apps/web/src/app/users/page.tsx
import { UserCard } from '@myapp/ui';
import { formatDate } from '@myapp/utils';
import type { User } from '@myapp/types';
async function getUsers(): Promise<User[]> {
const res = await fetch('/api/users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
subtitle={formatDate(user.createdAt)}
/>
))}
</div>
);
}
고수 팁
1. 패키지 버전 관리 전략
독립 버전: 각 패키지가 독립적인 버전 관리 (changesets 활용)
고정 버전: 모든 패키지가 같은 버전 (Lerna fixed mode)
2. changesets로 배포 관리
npm install --save-dev @changesets/cli
npx changeset init
# 변경 사항 기록
npx changeset add
# 버전 업데이트 + CHANGELOG 생성
npx changeset version
# 퍼블리시
npx changeset publish
3. 내부 패키지 빌드 vs 소스 직접 참조
// 개발 시: TS 소스 직접 참조 (빌드 없이 바로 반영)
{ "main": "./src/index.ts" }
// 배포 시: 빌드된 JS 참조
{ "main": "./dist/index.js", "types": "./dist/index.d.ts" }