Turborepo Monorepo: Next.js와 React 프로젝트 효율적으로 관리하기
Turborepo를 활용한 모노레포 구축 가이드를 통해 Next.js, React, TypeScript 기반 프론트엔드 프로젝트의 개발 효율성과 성능을 극대화하는 방법을 알아봅니다. 코드 공유, 캐싱, 병렬 실행 등 Turborepo의 핵심 기능을 실전 예제와 함께 자세히 설명합니다.
Turborepo Monorepo: Next.js와 React 프로젝트 효율적으로 관리하기
현대의 프론트엔드 개발은 복잡해지는 애플리케이션 구조와 빠르게 변화하는 기술 스택 속에서 효율적인 코드 관리와 개발 워크플로우를 요구합니다. 특히 여러 개의 웹 애플리케이션이나 라이브러리를 동시에 개발해야 하는 상황이라면, 각 프로젝트를 개별적으로 관리하는 폴리레포(Polyrepo) 방식은 여러 가지 비효율을 초래할 수 있습니다. 이러한 문제를 해결하기 위한 강력한 대안으로 모노레포(Monorepo) 전략이 주목받고 있으며, 그 중심에는 Turborepo가 있습니다.
이 글에서는 Turborepo를 활용하여 Next.js 및 React 기반의 프론트엔드 모노레포 환경을 구축하고, 공유 가능한 컴포넌트 라이브러리 및 유틸리티 패키지를 구성하는 방법을 심층적으로 다룹니다. Turborepo가 제공하는 뛰어난 빌드 성능과 캐싱 메커니즘을 이해하고, 실제 프로젝트에 적용하여 개발 생산성을 한 단계 끌어올리는 데 필요한 실질적인 가이드를 제공해 드리겠습니다.
모노레포(Monorepo)란 무엇인가요?
모노레포는 이름 그대로 '하나의 저장소(Repository)'를 의미하며, 여러 개의 프로젝트 코드를 단일 Git 저장소 내에서 관리하는 개발 전략입니다. 이는 각 프로젝트마다 별도의 저장소를 두는 폴리레포 방식과 대조됩니다.
모노레포는 다음과 같은 장점을 가집니다.
- 코드 공유 용이성: 공통으로 사용되는 UI 컴포넌트, 유틸리티 함수, 타입 정의 등을 별도의 패키지로 분리하여 여러 프로젝트에서 쉽게 재사용할 수 있습니다. 이는 중복 코드를 줄이고 일관성을 유지하는 데 큰 도움이 됩니다.
- 단일 버전 관리: 모든 프로젝트가 동일한 버전의 의존성을 사용하도록 강제하거나, 최소한 의존성 업데이트를 한 번에 관리할 수 있어 버전 불일치로 인한 문제를 줄입니다.
- 간소화된 의존성 관리: 루트 레벨에서 모든 의존성을 관리하거나, 특정 워크스페이스에 필요한 의존성만 설치하여
node_modules의 크기를 최적화할 수 있습니다. - 통합된 개발 경험: 모든 프로젝트의 코드가 한곳에 모여 있어 전체 시스템을 한눈에 파악하기 용이하며, IDE의 통합 기능(예: Go to Definition)을 활용하기 좋습니다.
- 원자적(Atomic) 커밋: 여러 프로젝트에 걸쳐 변경 사항이 발생했을 때, 하나의 커밋으로 모든 변경 사항을 묶어 관리할 수 있습니다.
물론, 모노레포에도 단점은 존재합니다. 저장소 크기가 커지고, 초기 설정 및 관리가 복잡해질 수 있으며, 빌드 시간이 길어질 수 있다는 점 등이 있습니다. 하지만 Turborepo와 같은 모노레포 도구들은 이러한 단점들을 효과적으로 보완하며 모노레포의 장점을 극대화합니다.
Turborepo는 왜 필요한가요?
Turborepo는 Vercel에서 개발한 고성능 빌드 시스템으로, 특히 JavaScript 및 TypeScript 기반의 모노레포 환경에서 탁월한 성능을 발휘합니다. 모노레포의 단점으로 지적되었던 긴 빌드 시간을 획기적으로 단축하고 개발 경험을 개선하는 데 초점을 맞춥니다.
Turborepo의 핵심 기능과 장점은 다음과 같습니다.
- 증분 빌드(Incremental Builds) 및 캐싱: Turborepo는 이전에 빌드된 결과물을 캐싱하고, 변경된 파일이 있는 프로젝트만 다시 빌드합니다. 이는
node_modules를 포함한 모든 파일의 해시를 기반으로 하므로, 변경 사항이 없는 프로젝트는 빌드 단계를 건너뛰어 빌드 시간을 대폭 줄여줍니다. 로컬 캐싱뿐만 아니라 원격 캐싱(Remote Caching)도 지원하여 CI/CD 환경에서도 이점을 제공합니다. - 병렬 실행(Parallel Execution): 여러 프로젝트의 빌드, 테스트, 린트 등의 태스크를 병렬로 실행하여 전체 작업 시간을 단축합니다. 프로젝트 간의 의존성 그래프를 분석하여 올바른 순서로 작업을 실행합니다.
- 작업 스케줄링 및 의존성 그래프: Turborepo는 모노레포 내의 프로젝트 간 의존성을 자동으로 파악하고, 이를 기반으로 태스크 실행 순서를 최적화합니다. 예를 들어,
ui패키지가web앱에 의해 사용된다면,ui패키지가 먼저 빌드된 후web앱이 빌드되도록 보장합니다. - 최소한의 설정:
turbo.json파일을 통해 직관적이고 간결한 설정만으로 강력한 기능을 활용할 수 있습니다. - 프레임워크 agnostic: Next.js, React, Vue, Svelte 등 특정 프레임워크에 종속되지 않고 어떤 JavaScript/TypeScript 프로젝트에도 적용할 수 있습니다.
이러한 기능들을 통해 Turborepo는 모노레포 환경에서 개발자가 직면하는 성능 및 관리 문제를 효과적으로 해결하며, 빠르고 효율적인 개발 환경을 제공합니다.
Turborepo 모노레포 환경 구축 시작하기
이제 Turborepo를 활용하여 새로운 모노레포 환경을 구축하는 과정을 살펴보겠습니다. Turborepo는 create-turbo CLI를 통해 손쉽게 시작할 수 있습니다.
먼저, 프로젝트를 생성할 디렉토리로 이동한 후 다음 명령어를 실행합니다. Yarn, npm, pnpm 등 원하는 패키지 매니저를 사용할 수 있습니다. 여기서는 pnpm을 기준으로 설명하겠습니다.
pnpm create turbo@latest
명령어를 실행하면 모노레포의 이름을 묻습니다. 예를 들어, my-turborepo라고 입력하면 해당 이름의 디렉토리가 생성되고, 그 안에 기본적인 Turborepo 구조가 초기화됩니다.
# my-turborepo 디렉토리 생성 및 초기화
# pnpm create turbo@latest
? Where would you like to create your turborepo? my-turborepo
생성된 디렉토리 구조는 대략 다음과 같을 것입니다.
my-turborepo/
├── apps/
│ ├── web/
│ └── docs/
├── packages/
│ ├── ui/
│ ├── config/
│ └── tsconfig/
├── turbo.json
├── package.json
├── pnpm-workspace.yaml
└── .gitignore
여기서 apps 디렉토리에는 실제 배포 가능한 애플리케이션(Next.js, React 앱 등)이 위치하고, packages 디렉토리에는 여러 apps에서 공유할 수 있는 코드(UI 컴포넌트, 유틸리티, 설정 파일 등)가 위치합니다.
package.json 파일에는 Turborepo 실행 스크립트와 워크스페이스(Workspace) 설정이 포함됩니다. pnpm-workspace.yaml 파일은 pnpm이 워크스페이스를 인식하도록 설정하는 역할을 합니다.
# pnpm-workspace.yaml 예시
packages:
- 'apps/*'
- 'packages/*'
이 설정은 pnpm에게 apps 디렉토리와 packages 디렉토리 내의 모든 하위 디렉토리를 워크스페이스로 인식하도록 지시합니다.
워크스페이스(Workspace) 이해 및 추가
Turborepo 모노레포에서 apps와 packages 디렉토리 내의 각 하위 디렉토리는 독립적인 워크스페이스가 됩니다. 각 워크스페이스는 자체적인 package.json 파일을 가지며, 고유한 의존성과 스크립트를 정의할 수 있습니다.
새로운 Next.js 앱 추가하기
apps 디렉토리에 새로운 Next.js 애플리케이션을 추가해 보겠습니다. create-next-app을 사용하여 my-next-app이라는 이름의 Next.js 프로젝트를 apps 디렉토리 안에 생성합니다.
cd apps
pnpm create next-app my-next-app --typescript --eslint --tailwind --app --use-pnpm
생성 후 apps/my-next-app 디렉토리가 생겼는지 확인합니다. 이제 my-next-app은 모노레포의 새로운 워크스페이스가 됩니다.
새로운 패키지(Package) 추가하기
공유 가능한 UI 컴포넌트를 담을 my-ui라는 이름의 패키지를 packages 디렉토리에 추가해 보겠습니다.
# packages 디렉토리로 이동
cd packages
# my-ui 디렉토리 생성
mkdir my-ui
cd my-ui
# package.json 파일 생성
pnpm init
# TypeScript 설정 및 React 타입 설치
pnpm add -D typescript react @types/react
packages/my-ui/package.json 파일은 다음과 같이 설정합니다.
// packages/my-ui/package.json
{
"name": "my-ui",
"version": "0.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
"license": "MIT",
"scripts": {
"lint": "eslint .",
"generate:component": "turbo gen react-component"
},
"devDependencies": {
"@types/react": "^18.2.37",
"react": "^18.2.0",
"typescript": "^5.2.2"
}
}
main과 types 필드는 이 패키지의 진입점을 나타냅니다. index.tsx 파일을 생성하고 간단한 컴포넌트를 추가합니다.
// packages/my-ui/index.tsx
import * as React from "react";
export * from "./Button";
// packages/my-ui/Button.tsx
import * as React from "react";
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
export const Button = ({ children, onClick }: ButtonProps) => {
return (
<button
onClick={onClick}
style={{
padding: "10px 20px",
borderRadius: "5px",
border: "1px solid #ccc",
backgroundColor: "#f0f0f0",
cursor: "pointer",
}}
>
{children}
</button>
);
};
이제 my-ui 패키지는 모노레포 내의 다른 워크스페이스에서 Button 컴포넌트를 공유할 수 있게 됩니다.
코드 공유를 위한 패키지(Package) 활용
새롭게 생성한 my-ui 패키지를 apps/my-next-app에서 사용하는 방법을 알아보겠습니다.
워크스페이스 의존성 추가
apps/my-next-app의 package.json 파일에 my-ui 패키지를 의존성으로 추가합니다. 이때, 일반적인 npm 패키지처럼 버전을 명시하는 대신, 워크스페이스 의존성을 나타내는 workspace:*를 사용합니다.
// apps/my-next-app/package.json
{
"name": "my-next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"my-ui": "workspace:*" // my-ui 패키지 의존성 추가
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}의존성을 추가한 후, 모노레포의 루트 디렉토리에서 pnpm install을 실행하여 의존성을 업데이트합니다. pnpm은 workspace:*를 인식하고 my-ui 패키지를 node_modules에 심볼릭 링크로 연결합니다.
# 모노레포 루트 디렉토리에서 실행
pnpm install
공유 컴포넌트 사용
이제 apps/my-next-app 내의 아무 컴포넌트나 페이지에서 my-ui 패키지의 Button 컴포넌트를 임포트하여 사용할 수 있습니다.
// apps/my-next-app/src/app/page.tsx
import { Button } from "my-ui"; // my-ui 패키지에서 Button 컴포넌트 임포트
export default function HomePage() {
return (
<div style={{ padding: "20px" }}>
<h1>Welcome to My Next.js App</h1>
<Button onClick={() => alert("Button clicked!")}>Click Me</Button>
</div>
);
}
apps/my-next-app 디렉토리로 이동하여 pnpm dev를 실행하면, 공유된 Button 컴포넌트가 Next.js 애플리케이션에 정상적으로 렌더링되는 것을 확인할 수 있습니다.
cd apps/my-next-app
pnpm dev
이처럼 packages 디렉토리 내에 공통으로 사용될 로직이나 UI 컴포넌트, 타입 정의 등을 패키지로 분리하면, apps 디렉토리 내의 여러 애플리케이션에서 일관된 코드를 쉽게 재사용할 수 있습니다.
Turborepo 태스크(Task) 설정 및 실행
Turborepo의 핵심 기능 중 하나는 turbo.json 파일을 통해 모노레포 내의 태스크(Task)를 효율적으로 관리하고 실행하는 것입니다. 빌드, 개발 서버 실행, 린트, 테스트 등의 공통 태스크를 turbo.json에 정의하여 Turborepo의 캐싱 및 병렬 실행 기능을 활용할 수 있습니다.
모노레포 루트에 있는 turbo.json 파일은 다음과 같은 기본 구조를 가집니다.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
각 속성의 의미는 다음과 같습니다.
-
pipeline: Turborepo가 실행할 태스크들을 정의합니다. -
build,lint,dev,test등: 각 태스크의 이름입니다. 이 이름은 워크스페이스package.json의scripts필드에 정의된 스크립트 이름과 일치해야 합니다. -
dependsOn: 현재 태스크가 실행되기 전에 먼저 완료되어야 하는 다른 태스크를 지정합니다.-
^build: 현재 워크스페이스의 의존성 그래프 상 상위에 있는 워크스페이스의build태스크가 먼저 실행되어야 함을 의미합니다. 이는 공유 패키지가 먼저 빌드되어야 앱이 빌드될 수 있는 경우에 유용합니다.
-
-
outputs: 태스크 실행 결과로 생성되는 파일이나 디렉토리를 지정합니다. Turborepo는 이outputs디렉토리를 캐싱하여 다음 실행 시 재사용합니다.-
dist/**:dist디렉토리 내의 모든 파일. -
.next/**: Next.js 빌드 결과물. -
!.next/cache/**:.next/cache디렉토리는 캐싱에서 제외 (Turborepo가 자체적으로 관리).
-
-
cache: 이 태스크의 결과물을 캐싱할지 여부를 결정합니다.dev서버처럼 지속적으로 실행되는 태스크는false로 설정하여 캐싱하지 않습니다. -
persistent: 태스크가 지속적으로 실행되어야 하는지 여부를 결정합니다 (예: 개발 서버).dev태스크에true로 설정하면,turbo run dev명령어가 백그라운드에서 계속 실행됩니다.
태스크 실행 예시
모노레포의 루트 디렉토리에서 turbo run 명령어를 사용하여 태스크를 실행할 수 있습니다.
모든 워크스페이스의 build 태스크 실행:
pnpm turbo run build
이 명령어는 apps와 packages 디렉토리 내의 모든 워크스페이스에서 package.json에 정의된 build 스크립트를 실행합니다. Turborepo는 dependsOn 설정을 기반으로 의존성 그래프를 분석하여 my-ui 패키지가 먼저 빌드된 후 my-next-app이 빌드되도록 순서를 최적화하고, 변경 사항이 없는 워크스페이스는 캐시된 결과물을 사용하여 빌드를 건너뜁니다.
특정 워크스페이스의 dev 태스크 실행:
pnpm turbo run dev --filter=my-next-app
--filter 옵션을 사용하여 특정 워크스페이스에 대해서만 태스크를 실행할 수 있습니다. 이 명령어는 apps/my-next-app 워크스페이스의 dev 스크립트만 실행합니다. --filter 옵션은 강력한 패턴 매칭 기능을 제공하여 특정 패턴의 워크스페이스만 선택할 수도 있습니다.
여러 워크스페이스의 dev 태스크 동시 실행:
pnpm turbo run dev --filter=my-next-app --filter=docs
이렇게 하면 my-next-app과 docs 앱의 개발 서버가 동시에 실행됩니다.
Turborepo의 태스크 설정은 모노레포의 복잡성을 관리하고 개발 워크플로우를 간소화하는 데 필수적인 역할을 합니다.
Next.js 및 React 프로젝트 통합
Turborepo 모노레포에서 Next.js 및 React 프로젝트를 통합하는 것은 앞서 설명한 워크스페이스 및 패키지 설정과 크게 다르지 않습니다. 핵심은 각 프로젝트를 독립적인 워크스페이스로 간주하고, 공유 가능한 코드나 설정을 packages 디렉토리 내의 패키지로 분리하는 것입니다.
TypeScript 설정 공유
모노레포 내의 모든 TypeScript 프로젝트가 일관된 설정을 사용하도록 packages/tsconfig와 같은 공유 패키지를 만들 수 있습니다.
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"esModuleInterop": true,
"isolatedModules": true,
"jsx": "preserve",
"lib": ["es2022", "dom"],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "es2022",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}
그리고 apps/my-next-app/tsconfig.json에서 이 기본 설정을 확장하여 사용합니다.
// apps/my-next-app/tsconfig.json
{
"extends": "tsconfig/base.json", // 공유 tsconfig 확장
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
"baseUrl": "." // Next.js 프로젝트의 기준 경로 설정
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
이렇게 하면 모든 프로젝트가 동일한 TypeScript 설정을 상속받아 일관된 개발 환경을 유지할 수 있습니다.
ESLint 및 Prettier 설정 공유
마찬가지로, ESLint 및 Prettier와 같은 코드 품질 도구의 설정도 packages/config와 같은 패키지에 모아 공유할 수 있습니다.
// packages/eslint-config-custom/index.js
module.exports = {
extends: ["next", "prettier"],
settings: {
react: {
version: "detect",
},
},
rules: {
// 여기에 공통 ESLint 규칙 추가
},
};
그리고 각 프로젝트의 .eslintrc.js에서 이 설정을 확장하여 사용합니다.
// apps/my-next-app/.eslintrc.js
module.exports = {
extends: ["custom"], // 공유 ESLint 설정 확장
};
이러한 방식으로 모든 프로젝트에 걸쳐 일관된 코드 스타일과 품질 기준을 적용할 수 있습니다.
마무리
Turborepo를 활용한 모노레포 구축은 복잡한 프론트엔드 프로젝트를 관리하고 개발 효율성을 극대화하는 강력한 전략입니다. 이 글에서 우리는 모노레포의 개념부터 Turborepo의 핵심 기능, 그리고 Next.js 및 React 프로젝트를 통합하는 실제적인 방법까지 살펴보았습니다.
Turborepo의 캐싱, 병렬 실행, 의존성 그래프 분석 등의 기능은 특히 대규모 프로젝트나 여러 개의 관련 프로젝트를 동시에 개발할 때 빛을 발합니다. 공유 가능한 패키지를 통해 코드 중복을 줄이고 일관성을 유지하며, 통합된 태스크 관리를 통해 개발 워크플로우를 간소화할 수 있습니다.
이 가이드가 여러분의 프론트엔드 개발 환경을 한 단계 더 발전시키는 데 도움이 되기를 바랍니다. Turborepo와 함께 더욱 빠르고 효율적인 개발 경험을 만들어나가시길 응원합니다.
관련 게시글
React Server Components (RSC) 심층 가이드: Next.js App Router 활용
React Server Components(RSC)의 개념, 동작 원리, 장점, 그리고 Next.js App Router에서 RSC를 활용하는 방법을 심층적으로 탐구합니다. 프론트엔드 개발의 새로운 패러다임을 이해하고 실전 코드 예제를 통해 RSC를 마스터하세요.
Progressive Web App (PWA) 구축 실전: Next.js와 React를 활용한 PWA 개발 가이드
Next.js와 React 기반 PWA 구축 실전 가이드. Service Worker, Web App Manifest 설정 및 next-pwa 활용법을 통해 사용자 경험을 향상시키세요.
CSS Container Queries: 반응형 웹 디자인의 새로운 지평
CSS Container Queries를 활용하여 컴포넌트 기반 반응형 웹 디자인을 구현하는 방법을 심층적으로 알아봅니다. React, Next.js 환경에서 실전 예제를 통해 강력한 기능을 경험하세요.