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와 함께 더욱 빠르고 효율적인 개발 경험을 만들어나가시길 응원합니다.
관련 게시글
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 애플리케이션의 프론트엔드 기능을 강화하세요.