Turborepo Monorepo 구축 가이드: React, Next.js 프로젝트를 위한 고성능 워크스페이스
Turborepo를 활용하여 React, Next.js, TypeScript 기반의 고성능 모노레포를 구축하는 방법을 상세히 안내합니다. 공유 컴포넌트, 설정, 캐싱 전략을 포함한 실전 가이드입니다.
Turborepo Monorepo 구축 가이드: React, Next.js 프로젝트를 위한 고성능 워크스페이스
현대 웹 개발 환경에서는 여러 프로젝트를 효율적으로 관리하고 코드 재사용성을 극대화하는 것이 중요해졌습니다. 특히 프론트엔드 프로젝트들이 복잡해지고 규모가 커지면서, 단일 저장소에서 여러 프로젝트를 관리하는 모노레포(Monorepo) 전략이 각광받고 있습니다. 이 글에서는 고성능 빌드 시스템인 Turborepo를 활용하여 React, Next.js 애플리케이션과 공유 라이브러리를 포함하는 모노레포를 구축하는 방법을 상세히 안내해 드립니다.
1. 모노레포(Monorepo)와 Turborepo의 등장 배경
모노레포는 여러 프로젝트의 코드를 하나의 Git 저장소에서 관리하는 개발 전략입니다. 이는 마이크로 서비스 아키텍처나 여러 클라이언트(웹, 모바일 등)를 위한 공통 로직 및 UI 컴포넌트를 개발할 때 특히 유용합니다.
모노레포의 장점
- 코드 재사용성 증대: 공통 유틸리티 함수, UI 컴포넌트, 타입 정의 등을 패키지로 분리하여 여러 프로젝트에서 쉽게 공유하고 재사용할 수 있습니다.
- 단일 의존성 관리: 모든 프로젝트의 의존성을 한곳에서 관리하여 버전 불일치 문제를 줄이고, 업데이트를 용이하게 합니다.
- 일관된 개발 환경: 모든 프로젝트에 동일한 린트, 포맷터, 빌드 설정을 적용하여 코드 스타일과 품질을 일관되게 유지할 수 있습니다.
- 간소화된 배포: 관련 프로젝트들을 함께 배포하거나, 변경된 프로젝트만 효율적으로 배포할 수 있습니다.
모노레포의 도전 과제와 Turborepo의 역할
모노레포는 위와 같은 많은 장점에도 불구하고, 프로젝트 수가 늘어날수록 빌드 시간 증가, 복잡한 의존성 관리, 개발 환경 설정의 어려움 등 성능 및 관리의 어려움이 발생할 수 있습니다. 이러한 문제를 해결하기 위해 Yarn Workspaces, Lerna와 같은 도구들이 등장했지만, 대규모 프로젝트에서는 여전히 빌드 성능이 병목이 되곤 했습니다.
여기서 Turborepo가 등장합니다. Turborepo는 Vercel에서 개발한 고성능 빌드 시스템으로, 다음과 같은 핵심 기능을 통해 모노레포의 단점을 극복합니다.
- 증분 빌드(Incremental Builds): 변경된 부분만 다시 빌드하고, 변경되지 않은 부분은 이전 빌드 결과를 재사용합니다.
- 원격 캐싱(Remote Caching): 로컬 캐싱을 넘어 빌드 결과를 원격 서버에 저장하고 공유하여, 팀원 간 또는 CI/CD 파이프라인에서 빌드 시간을 획기적으로 단축합니다.
- 병렬 실행(Parallel Execution): 의존성 그래프를 분석하여 독립적인 태스크들을 동시에 실행하여 빌드 속도를 최적화합니다.
- 제로 컨피그(Zero-config)에 가까운 설정: 최소한의 설정으로 강력한 기능을 제공하여 모노레포 설정을 간소화합니다.
이러한 Turborepo의 강점을 활용하여 React, Next.js 기반의 프론트엔드 모노레포를 효율적으로 구축해 보겠습니다.
2. Turborepo Monorepo 초기 설정
Turborepo는 create-turbo CLI를 통해 손쉽게 초기 설정을 할 수 있습니다.
프로젝트 초기화
먼저, 프로젝트를 생성할 디렉토리로 이동하여 다음 명령어를 실행합니다.
# pnpm 사용을 권장합니다.
npm install -g pnpm
pnpm create turbo@latest
명령어를 실행하면 프로젝트 이름, 패키지 매니저 선택 등의 질문이 나옵니다.
-
Where would you like to create your turborepo?(프로젝트 이름 입력) -
Which package manager would you like to use?(pnpm 선택)
초기화가 완료되면 다음과 같은 기본 디렉토리 구조가 생성됩니다.
my-turborepo/
├── apps/
│ ├── docs/ (Next.js 예시 앱)
│ └── web/ (Next.js 예시 앱)
├── packages/
│ ├── eslint-config-custom/ (공유 ESLint 설정)
│ ├── tsconfig/ (공유 TypeScript 설정)
│ └── ui/ (공유 UI 컴포넌트)
├── .gitignore
├── package.json
├── pnpm-lock.yaml
└── turbo.json
package.json 및 turbo.json 이해하기
package.json (루트)
루트 package.json 파일에는 workspaces 속성이 정의되어 있습니다. 이 속성은 pnpm(또는 yarn, npm)에게 apps/와 packages/ 디렉토리에 있는 서브 프로젝트들을 워크스페이스로 인식하도록 지시합니다.
// my-turborepo/package.json
{
"name": "my-turborepo",
"version": "0.0.0",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"prettier": "^3.1.1",
"turbo": "latest"
},
"packageManager": "pnpm@8.15.4"
}
-
workspaces: 모노레포 내의 서브 프로젝트 경로를 정의합니다. -
scripts:turbo명령어를 사용하여build,dev,lint등의 스크립트를 워크스페이스 전체에 적용할 수 있습니다.
turbo.json (루트)
turbo.json은 Turborepo의 핵심 설정 파일입니다. 이곳에서 빌드 파이프라인, 캐싱 전략 등을 정의합니다.
// my-turborepo/turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
-
pipeline: 각 스크립트(예:build,lint,dev)에 대한 동작을 정의합니다.-
dependsOn: 해당 스크립트가 실행되기 전에 선행되어야 할 다른 패키지의 스크립트를 정의합니다.^는 "모든 종속성"을 의미합니다. 예를 들어,^build는 현재 패키지가 의존하는 모든 패키지의build스크립트가 먼저 실행되어야 함을 뜻합니다. -
outputs: 캐싱할 결과물을 지정합니다.dist/,.next/등 빌드 결과물이 저장되는 경로를 정의합니다. -
cache: 이 스크립트의 결과를 캐시할지 여부를 결정합니다.dev스크립트는 지속적으로 실행되어야 하므로false로 설정합니다. -
persistent: 스크립트가 지속적으로 실행되어야 하는지 여부를 나타냅니다 (예: 개발 서버).
-
3. Next.js 애플리케이션 추가 및 설정
apps/web 디렉토리에 Next.js 애플리케이션을 설정해 보겠습니다. create-turbo로 생성된 기본 템플릿에는 이미 Next.js 앱이 포함되어 있으므로, 이를 기반으로 설명합니다.
apps/web 디렉토리 구조
apps/web/
├── public/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── globals.css
├── .eslintrc.js
├── next.config.js
├── package.json
└── tsconfig.json
apps/web/package.json
web 앱의 package.json은 Next.js 애플리케이션에 필요한 의존성과 스크립트를 정의합니다.
// apps/web/package.json
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ui": "workspace:*" // 중요: 로컬 패키지 의존성
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.4",
"@types/node": "^17.0.12",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*", // 공유 ESLint 설정
"tsconfig": "workspace:*", // 공유 TypeScript 설정
"typescript": "^5.3.3"
}
}
-
ui: "workspace:":packages/ui디렉토리에 정의될 공유 UI 패키지에 대한 의존성을 나타냅니다.workspace:는 pnpm에게 로컬 워크스페이스에서ui패키지를 찾도록 지시합니다. -
eslint-config-custom: "workspace:"및tsconfig: "workspace:": 공유 ESLint 및 TypeScript 설정 패키지에 대한 의존성입니다.
apps/web/tsconfig.json
공유 TypeScript 설정을 확장하여 사용합니다.
// apps/web/tsconfig.json
{
"extends": "tsconfig/next.json", // 공유 next.json 설정 확장
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
next.config.js 파일도 생성되지만, 여기서는 기본 설정을 유지합니다. 이제 pnpm dev 명령어를 루트에서 실행하면 turbo dev가 호출되고, apps/web의 Next.js 개발 서버가 실행됩니다.
4. 공유 UI 컴포넌트 Package 생성
모노레포의 가장 큰 장점 중 하나는 UI 컴포넌트를 공유하여 일관된 디자인 시스템을 구축하고 개발 속도를 높이는 것입니다. packages/ui 디렉토리에 공유 React UI 컴포넌트를 만들어 보겠습니다.
packages/ui/package.json
// packages/ui/package.json
{
"name": "ui",
"version": "0.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"license": "MIT",
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"dev": "tsup src/index.tsx --format esm,cjs --dts --external react --watch",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf dist"
},
"devDependencies": {
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"react": "^18.2.0",
"tsconfig": "workspace:*",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
},
"publishConfig": {
"access": "public"
}
}
-
main,module,types: 빌드된 패키지의 진입점과 타입 정의 파일을 지정합니다. -
tsup: TypeScript 코드를 JavaScript로 번들링하는 데 사용되는 도구입니다.tsup src/index.tsx --format esm,cjs --dts --external react명령은src/index.tsx를 ESM과 CommonJS 형식으로 빌드하고, 타입 정의 파일(dts)을 생성하며,react를 외부 의존성으로 처리합니다.
packages/ui/tsconfig.json
// packages/ui/tsconfig.json
{
"extends": "tsconfig/react-library.json", // 공유 react-library.json 설정 확장
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}
packages/ui/src/index.tsx
간단한 Button 컴포넌트를 생성합니다.
// packages/ui/src/index.tsx
import * as React from "react";
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = "primary",
}) => {
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}`} onClick={onClick}>
{children}
</button>
);
};
apps/web에서 ui 패키지 사용하기
apps/web/src/app/page.tsx 파일에서 ui 패키지의 Button 컴포넌트를 가져와 사용합니다.
// apps/web/src/app/page.tsx
import { Button } from "ui"; // 'ui' 패키지를 직접 임포트
export default function Web() {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold mb-8">Welcome to Turborepo with Next.js!</h1>
<Button onClick={() => alert("Button clicked!")} variant="primary">
Click Me (Primary)
</Button>
<Button onClick={() => alert("Another button clicked!")} variant="secondary" className="ml-4">
Click Me (Secondary)
</Button>
</div>
);
}
이제 pnpm dev를 실행하고 http://localhost:3000에 접속하면 ui 패키지의 Button 컴포넌트가 정상적으로 렌더링되는 것을 확인할 수 있습니다.
5. 공유 설정 Package 생성 (ESLint, TypeScript)
모노레포의 또 다른 강력한 기능은 공통 개발 설정을 공유하여 일관된 코드 품질을 유지하는 것입니다. packages/eslint-config-custom과 packages/tsconfig는 이러한 공유 설정을 위한 패키지입니다.
packages/eslint-config-custom
이 패키지는 ESLint 설정을 공유합니다.
// packages/eslint-config-custom/index.js
module.exports = {
extends: ["next", "prettier"],
settings: {
react: {
version: "detect",
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
},
};
-
extends:next(Next.js 권장 설정)와prettier(prettier와 충돌하는 룰 비활성화)를 확장합니다. - 각
apps나packages의.eslintrc.js파일에서 이 설정을extends합니다.
// apps/web/.eslintrc.js
module.exports = {
extends: ["custom"], // 'custom'은 packages/eslint-config-custom을 가리킵니다.
};
packages/tsconfig
이 패키지는 TypeScript 컴파일러 설정을 공유합니다. base.json, next.json, react-library.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": "preserve",
"module": "esnext",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
}
}
// packages/tsconfig/next.json
{
"extends": "./base.json",
"display": "Next.js",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
}
}
apps/web/tsconfig.json 파일에서 extends: "tsconfig/next.json"와 같이 이 설정을 확장하여 사용합니다.
6. Turborepo의 캐싱 및 병렬 처리 활용
Turborepo의 진정한 가치는 빌드 속도 향상에 있습니다. turbo.json에 정의된 파이프라인과 캐싱 기능을 통해 이를 경험할 수 있습니다.
빌드 실행 및 캐싱 확인
루트 디렉토리에서 다음 명령어를 실행하여 모든 프로젝트를 빌드합니다.
pnpm build
처음 빌드를 실행하면 모든 프로젝트가 빌드됩니다. 출력 메시지에서 각 프로젝트의 빌드 시간이 표시되고, (cached) 표시 없이 진행될 것입니다.
• Packages in scope: web, ui, eslint-config-custom, tsconfig
• Running build#build in 4 packages
...
Successfully ran 4 tasks in 20s
이제 다시 pnpm build를 실행해 보세요.
pnpm build
이번에는 대부분의 태스크가 (cached)로 표시되며, 빌드 시간이 획기적으로 단축되는 것을 볼 수 있습니다.
• Packages in scope: web, ui, eslint-config-custom, tsconfig
• build#web:cache Caching enabled for task build#web
• build#tsconfig:cache Caching enabled for task build#tsconfig
• build#eslint-config-custom:cache Caching enabled for task build#eslint-config-custom
• build#ui:cache Caching enabled for task build#ui
...
Successfully ran 4 tasks in 0.5s (4 cached, 0 remote)
이는 Turborepo가 이전 빌드 결과를 캐시하고, 변경 사항이 없는 프로젝트는 캐시된 결과를 재사용하기 때문입니다.
원격 캐싱(Remote Caching)
Turborepo는 Vercel 계정과 연동하여 원격 캐싱을 지원합니다. turbo login 명령어를 통해 Vercel 계정에 로그인하고, turbo link 명령어로 프로젝트를 연결하면 팀원 간 또는 CI/CD 환경에서 빌드 캐시를 공유할 수 있습니다. 이는 특히 대규모 팀에서 CI/CD 빌드 시간을 크게 단축시키는 데 기여합니다.
7. CSS/Tailwind CSS 통합
프론트엔드 프로젝트에서 CSS 프레임워크는 필수적입니다. Tailwind CSS를 공유 UI 패키지에 통합하여 사용해 보겠습니다.
packages/ui에 Tailwind CSS 설정
- Tailwind CSS 설치:
packages/ui 디렉토리로 이동하여 Tailwind CSS 및 관련 의존성을 설치합니다.
cd packages/ui
pnpm add -D tailwindcss postcss autoprefixer
pnpm tailwindcss init -p
-
packages/ui/tailwind.config.js설정:
ui 패키지의 컴포넌트 파일들을 스캔하도록 content를 설정합니다.
// packages/ui/tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
-
packages/ui/postcss.config.js설정:
// packages/ui/postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
-
packages/ui/src/styles.css(또는global.css) 생성:
Tailwind CSS 지시어를 추가합니다.
/* packages/ui/src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
-
packages/ui/src/index.tsx에서 CSS 임포트:
index.tsx 파일에서 위에서 만든 styles.css를 임포트합니다.
// packages/ui/src/index.tsx
import * as React from "react";
import "./styles.css"; // Tailwind CSS 임포트
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = "primary",
}) => {
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}`} onClick={onClick}>
{children}
</button>
);
};
apps/web에서 Tailwind CSS 사용 및 공유 설정 확장
-
apps/web에 Tailwind CSS 설정:
apps/web 디렉토리로 이동하여 Tailwind CSS 및 관련 의존성을 설치합니다.
cd apps/web
pnpm add -D tailwindcss postcss autoprefixer
pnpm tailwindcss init -p
-
apps/web/tailwind.config.js설정:
ui 패키지의 tailwind.config.js를 확장하고, web 앱의 파일들도 스캔하도록 설정합니다.
// apps/web/tailwind.config.js
const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
// ui 패키지의 tailwind.config.js를 확장합니다.
// 이 방식은 PostCSS의 @import를 사용하거나, 직접 병합하는 방식이 있습니다.
// 여기서는 직접 컨텐츠를 명시하는 방식을 사용합니다.
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// ui 패키지의 src 디렉토리를 포함합니다.
join(__dirname, "../../packages/ui/src/**/*.{js,ts,jsx,tsx}")
],
theme: {
extend: {},
},
plugins: [],
};
-
apps/web/src/app/globals.css에 Tailwind CSS 지시어 추가:
/* apps/web/src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
이제 pnpm dev를 실행하면 ui 패키지의 컴포넌트와 apps/web 앱 모두에서 Tailwind CSS 스타일이 정상적으로 적용되는 것을 확인할 수 있습니다.
마무리
이 가이드에서는 Turborepo를 활용하여 React, Next.js, TypeScript 기반의 모노레포를 구축하는 과정을 상세히 살펴보았습니다. 초기 설정부터 공유 UI 컴포넌트, 공유 ESLint 및 TypeScript 설정, 그리고 Turborepo의 강력한 캐싱 및 병렬 처리 기능, Tailwind CSS 통합까지 다루며 효율적인 개발 환경을 구축하는 방법을 알아보았습니다.
Turborepo 모노레포는 코드 재사용성을 높이고, 개발 일관성을 유지하며, 빌드 속도를 획기적으로 단축하여 대규모 프론트엔드 프로젝트의 생산성을 극대화하는 데 큰 도움을 줍니다. 이 가이드를 통해 여러분의 프로젝트에 Turborepo를 성공적으로 적용하시길 바랍니다.
관련 게시글
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 애플리케이션의 프론트엔드 기능을 강화하세요.