Turborepo Monorepo: React, Next.js 프로젝트를 위한 효율적인 구축 가이드
Turborepo를 활용한 Monorepo 구축 방법을 상세히 다루며, React, Next.js 기반의 프론트엔드 프로젝트 관리를 위한 TypeScript, JavaScript, CSS 설정 및 최적화 전략을 소개합니다.
Turborepo Monorepo: React, Next.js 프로젝트를 위한 효율적인 구축 가이드
현대 웹 개발에서 프로젝트의 규모가 커지고 여러 애플리케이션 및 라이브러리가 유기적으로 연결될수록, 효과적인 코드 관리와 빌드 프로세스는 개발 생산성에 지대한 영향을 미칩니다. 이러한 복잡성을 해결하기 위해 Monorepo(모노레포) 아키텍처가 주목받고 있으며, 그중에서도 Turborepo는 뛰어난 성능과 개발자 경험을 제공하는 강력한 도구로 자리매김하고 있습니다. 이 글에서는 Turborepo를 활용하여 React 및 Next.js 기반의 프론트엔드 프로젝트를 Monorepo로 효율적으로 구축하는 방법을 상세히 안내해 드립니다.
Monorepo와 Turborepo의 이해
Monorepo란 무엇인가?
Monorepo는 여러 프로젝트의 코드를 단일 저장소(repository)에서 관리하는 개발 전략입니다. 이는 개별 저장소(Polyrepo) 방식과 대비되는 개념으로, 다음과 같은 장단점을 가집니다.
| 특징 | Monorepo | Polyrepo |
|---|---|---|
| 코드 공유 | 모든 프로젝트가 쉽게 코드와 설정 공유 | 각 프로젝트마다 중복 코드 발생 가능성 높음 |
| 버전 관리 | 모든 프로젝트가 동일한 버전 관리 시스템 사용 | 각 프로젝트마다 개별 버전 관리 |
| 일관성 | 공통 설정(ESLint, Prettier, TypeScript) 적용 용이 | 프로젝트별 설정 불일치 발생 가능성 높음 |
| 의존성 관리 | 단일 node_modules로 중복 설치 방지 및 관리 용이 | 각 프로젝트마다 node_modules 개별 관리 |
| 빌드/테스트 | 전체 프로젝트에 대한 통합 빌드/테스트 가능 | 각 프로젝트별 개별 빌드/테스트 수행 |
| 초기 설정 | 설정 복잡도가 높을 수 있음 | 비교적 간단 |
| 커밋 히스토리 | 단일 히스토리로 인해 커밋 수가 많아질 수 있음 | 프로젝트별 독립적인 히스토리 |
Monorepo는 특히 프론트엔드 애플리케이션, 백엔드 API, 공유 UI 컴포넌트 라이브러리 등 여러 구성 요소가 긴밀하게 연동되는 대규모 프로젝트에서 빛을 발합니다.
Turborepo는 왜 필요한가?
Monorepo의 장점에도 불구하고, 여러 프로젝트를 한곳에 모아두면 빌드 시간 증가, 의존성 관리 복잡성 등의 문제가 발생할 수 있습니다. Turborepo는 이러한 Monorepo의 단점을 극복하고 장점을 극대화하기 위해 설계된 고성능 빌드 시스템입니다.
Turborepo의 핵심 강점은 다음과 같습니다.
- 증분 빌드 (Incremental Builds): 변경된 부분만 다시 빌드하여 빌드 시간을 획기적으로 단축합니다.
- 원격 캐싱 (Remote Caching): 팀원 간, CI/CD 환경 간 빌드 캐시를 공유하여 빌드 시간을 더욱 절약합니다.
- 병렬 실행 (Parallel Execution): 의존성이 없는 작업들을 병렬로 실행하여 전체 빌드 시간을 최적화합니다.
- 제로 설정 (Zero-config): 대부분의 Node.js 및 JavaScript 프로젝트에서 별도의 복잡한 설정 없이 바로 사용할 수 있습니다.
이러한 기능들을 통해 Turborepo는 Monorepo 환경에서 개발 생산성을 비약적으로 향상시킵니다.
Turborepo Monorepo 초기 설정
Turborepo 프로젝트를 시작하는 가장 쉬운 방법은 create-turbo CLI를 사용하는 것입니다.
npx create-turbo@latest
명령어를 실행하면 프로젝트 이름을 입력하라고 나옵니다. 예를 들어 my-turborepo라고 입력하면 다음과 같은 기본 구조의 프로젝트가 생성됩니다.
my-turborepo
├── apps
│ ├── docs
│ └── web
├── packages
│ ├── eslint-config-custom
│ ├── tsconfig
│ └── ui
├── .gitignore
├── package.json
├── pnpm-lock.yaml
└── turbo.json
-
apps/: 실제 실행 가능한 애플리케이션(Next.js, React, Express 등)을 포함하는 디렉토리입니다. -
packages/: 여러apps에서 공유될 공통 코드, UI 컴포넌트 라이브러리, 설정 파일 등을 포함하는 디렉토리입니다. -
turbo.json: Turborepo의 핵심 설정 파일로, 빌드 파이프라인과 캐싱 전략을 정의합니다. -
package.json: Monorepo 전체의 의존성 관리와 스크립트를 정의합니다.workspaces필드를 통해apps와packages디렉토리를 Monorepo의 구성 요소로 인식시킵니다.
초기 package.json은 다음과 같은 형태를 가집니다.
// package.json (root)
{
"name": "my-turborepo",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\""
},
"devDependencies": {
"prettier": "^3.0.3",
"turbo": "latest"
},
"packageManager": "pnpm@8.9.0",
"workspaces": [
"apps/*",
"packages/*"
]
}
workspaces 필드가 apps/와 packages/를 지정하여 Monorepo 내의 모든 서브 프로젝트를 관리합니다.
Workspace 추가: React & Next.js 애플리케이션
이제 apps 디렉토리에 Next.js 및 React 애플리케이션을 추가해 보겠습니다.
Next.js 애플리케이션 추가
apps/nextjs-app 디렉토리를 생성하고, Next.js 프로젝트를 초기화합니다.
cd apps
npx create-next-app@latest nextjs-app --ts --eslint --tailwind --app --src-dir --import-alias "@/*"
Next.js 설치 시 Turborepo와 관련된 몇 가지 설정을 조정해야 합니다. apps/nextjs-app/package.json 파일의 name 필드를 nextjs-app으로 변경하고, devDependencies에 eslint-config-custom과 tsconfig 패키지를 추가합니다.
// apps/nextjs-app/package.json
{
"name": "nextjs-app", // 변경
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@repo/ui": "*", // 나중에 추가할 공유 UI 패키지
// ... Next.js 관련 의존성
},
"devDependencies": {
"@repo/eslint-config-custom": "workspace:*", // Monorepo 공유 ESLint 설정
"@repo/tsconfig": "workspace:*", // Monorepo 공유 TypeScript 설정
// ... Next.js 개발 의존성
}
}
workspace:*는 Monorepo 내의 다른 패키지를 참조할 때 사용하며, Turborepo가 이를 자동으로 관리합니다.
React 애플리케이션 추가 (Vite 사용)
apps/react-app 디렉토리에 Vite를 사용하여 React 프로젝트를 초기화합니다.
cd apps
npm create vite@latest react-app -- --template react-ts
마찬가지로 apps/react-app/package.json 파일의 name 필드를 react-app으로 변경하고, devDependencies에 공유 패키지를 추가합니다.
// apps/react-app/package.json
{
"name": "react-app", // 변경
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@repo/ui": "*", // 나중에 추가할 공유 UI 패키지
// ... React 관련 의존성
},
"devDependencies": {
"@repo/eslint-config-custom": "workspace:*",
"@repo/tsconfig": "workspace:*",
// ... React 개발 의존성
}
}
루트 디렉토리로 돌아와 pnpm install 또는 npm install을 실행하여 모든 의존성을 설치합니다.
cd ../.. # go back to root
pnpm install # or npm install
Shared Packages 구축: UI 컴포넌트 라이브러리
Monorepo의 가장 큰 장점 중 하나는 코드 재사용성입니다. packages 디렉토리에 공유 UI 컴포넌트 라이브러리를 구축해 보겠습니다.
ui 패키지 생성 및 설정
packages/ui 디렉토리에 다음과 같은 package.json을 생성합니다.
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"license": "MIT",
"scripts": {
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"generate:component": "turbo gen react-component"
},
"devDependencies": {
"@repo/eslint-config-custom": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"eslint": "^8.53.0",
"react": "^18.2.0",
"typescript": "^5.2.2"
}
}
name 필드는 @repo/ui와 같이 스코프 패키지(scoped package) 형태로 지정하는 것이 좋습니다. main과 types 필드는 이 패키지의 진입점과 타입 정의 파일을 가리킵니다.
packages/ui/src/index.tsx 파일을 생성하여 간단한 Button 컴포넌트를 작성합니다.
// packages/ui/src/Button.tsx
import * as React from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: "primary" | "secondary";
}
export const Button = ({ children, variant = "primary", ...props }: ButtonProps) => {
const baseStyle = "px-4 py-2 rounded-md font-semibold";
const variantStyle =
variant === "primary"
? "bg-blue-500 text-white hover:bg-blue-600"
: "bg-gray-200 text-gray-800 hover:bg-gray-300";
return (
<button className={`${baseStyle} ${variantStyle}`} {...props}>
{children}
</button>
);
};
// packages/ui/src/index.tsx
export * from "./Button";
애플리케이션에서 ui 패키지 사용
이제 nextjs-app과 react-app에서 @repo/ui 패키지를 사용할 수 있습니다.
Next.js 애플리케이션:
apps/nextjs-app/src/app/page.tsx에서 @repo/ui의 Button 컴포넌트를 임포트하여 사용합니다.
// apps/nextjs-app/src/app/page.tsx
import { Button } from "@repo/ui"; // @repo/ui 패키지 임포트
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1 className="text-4xl font-bold mb-8">Next.js App with Turborepo UI</h1>
<Button onClick={() => alert("Hello from Next.js!")}>Click Me (Primary)</Button>
<Button variant="secondary" className="mt-4" onClick={() => alert("Secondary button clicked!")}>
Click Me (Secondary)
</Button>
</main>
);
}
React 애플리케이션:
apps/react-app/src/App.tsx에서 Button 컴포넌트를 임포트하여 사용합니다.
// apps/react-app/src/App.tsx
import { useState } from 'react'
import { Button } from "@repo/ui"; // @repo/ui 패키지 임포트
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<h1>React App with Turborepo UI</h1>
<div className="card">
<Button onClick={() => setCount((count) => count + 1)}>
count is {count}
</Button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
</>
)
}
export default App
각 애플리케이션의 package.json에 tailwindcss와 같은 CSS 프레임워크를 추가하고 설정하여 UI 컴포넌트의 스타일링을 적용할 수 있습니다.
Shared Packages 구축: Configuration & Utilities
코드 공유를 넘어서, 개발 환경의 일관성을 유지하기 위해 공통 설정 파일들도 packages 디렉토리에 관리할 수 있습니다.
eslint-config-custom
packages/eslint-config-custom 디렉토리에 다음과 같은 index.js 파일을 생성합니다.
// packages/eslint-config-custom/index.js
module.exports = {
extends: ["next", "prettier", "turbo"],
rules: {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
},
parserOptions: {
babelOptions: {
presets: [require.resolve("next/babel")],
},
},
};
이 설정은 Next.js, Prettier, Turborepo에서 권장하는 ESLint 규칙을 상속받아 사용합니다. 각 애플리케이션의 .eslintrc.js 파일에서 이 설정을 확장하여 사용합니다.
// apps/nextjs-app/.eslintrc.js
module.exports = {
extends: ["@repo/eslint-config-custom"],
};
tsconfig
packages/tsconfig 디렉토리에 공유 TypeScript 설정 파일을 생성합니다. 예를 들어 base.json 파일을 다음과 같이 작성합니다.
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,
"noEmit": true,
"lib": ["es2022", "dom", "dom.iterable"],
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}
각 애플리케이션의 tsconfig.json 파일에서 이 공유 설정을 확장합니다.
// apps/nextjs-app/tsconfig.json
{
"extends": "@repo/tsconfig/base.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
utils 패키지 (선택 사항)
자주 사용되는 유틸리티 함수(날짜 포맷팅, API 호출 헬퍼 등)들도 packages/utils와 같은 디렉토리에 모아 관리할 수 있습니다.
// packages/utils/package.json
{
"name": "@repo/utils",
"version": "0.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"license": "MIT",
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"typescript": "^5.2.2"
}
}
// packages/utils/src/formatDate.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString("ko-KR", {
year: "numeric",
month: "long",
day: "numeric",
});
}
// packages/utils/src/index.ts
export * from "./formatDate";
이렇게 하면 모든 애플리케이션에서 일관된 유틸리티를 사용할 수 있습니다.
Turborepo의 캐싱 및 병렬 처리
turbo.json 파일은 Turborepo의 성능을 좌우하는 핵심 설정입니다. 이 파일은 Monorepo 내의 작업을 정의하고, 각 작업이 어떻게 실행되고 캐시될지 지시합니다.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
-
pipeline: Monorepo 내에서 실행될 작업(스크립트)과 그들의 의존성, 캐싱 전략을 정의합니다. -
build:-
dependsOn:^build는 모든 워크스페이스의build작업이 현재 워크스페이스의build작업보다 먼저 실행되어야 함을 의미합니다. 이는 공유 패키지가 먼저 빌드되어야 애플리케이션이 빌드될 수 있음을 보장합니다. -
outputs: 빌드 결과물이 저장될 디렉토리를 지정합니다. 이 디렉토리의 내용이 캐시됩니다.
-
-
lint:outputs를 비워두면, lint 작업은 결과물을 생성하지 않으므로 캐시될 필요가 없음을 나타냅니다. -
dev:-
cache: false: 개발 서버는 지속적으로 실행되므로 캐시할 필요가 없습니다. -
persistent: true: 이 작업이 지속적으로 실행되는 프로세스임을 나타냅니다 (예:next dev,vite).
-
-
test:build작업에 의존하며, 테스트 결과물(coverage)을 캐시합니다.
이러한 설정을 통해 Turborepo는 변경된 파일만 식별하여 해당 워크스페이스의 작업만 다시 실행하고, 이전에 실행된 결과는 캐시에서 가져와 빌드 시간을 최소화합니다.
스크립트 실행 및 개발 워크플로우
루트 package.json의 scripts 섹션은 Turborepo 명령어를 사용하여 Monorepo 전체의 작업을 관리합니다.
// package.json (root)
{
// ...
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\""
},
// ...
}
-
pnpm dev(또는npm run dev): 모든 워크스페이스의dev스크립트를 병렬로 실행합니다.apps/nextjs-app과apps/react-app의 개발 서버가 동시에 시작됩니다. -
pnpm build(또는npm run build): 모든 워크스페이스의build스크립트를 정의된 의존성(^build)에 따라 최적화된 순서로 실행하고 캐시합니다. -
pnpm lint: 모든 워크스페이스의lint스크립트를 실행합니다.
특정 워크스페이스의 스크립트만 실행하려면 --filter 옵션을 사용합니다.
# nextjs-app만 개발 서버 실행
pnpm dev --filter=nextjs-app
# ui 패키지만 빌드
pnpm build --filter=@repo/ui
Turborepo는 Monorepo 환경에서 개발, 빌드, 테스트, 배포 과정을 간소화하고 가속화하여 개발팀의 생산성을 극대화합니다.
마무리
이 글에서는 Turborepo를 활용하여 React 및 Next.js 기반의 프론트엔드 Monorepo를 구축하는 과정을 상세히 살펴보았습니다. Monorepo의 개념부터 Turborepo의 초기 설정, 공유 UI 컴포넌트 및 설정 패키지 구축, 그리고 Turborepo의 핵심 기능인 캐싱과 병렬 처리를 통한 효율적인 워크플로우까지 다루었습니다.
Turborepo는 복잡한 다중 프로젝트 환경에서 일관된 개발 경험을 제공하고, 빌드 시간을 획기적으로 단축하여 개발 생산성을 크게 향상시킬 수 있는 강력한 도구입니다. 이 가이드를 통해 여러분의 프로젝트에 Turborepo Monorepo를 성공적으로 도입하고, 더욱 효율적인 웹 개발 환경을 구축하시길 바랍니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.