DevToolBox무료
블로그

Monorepo 가이드 2026: Turborepo vs Nx

15분by DevToolBox

Monorepo Guide 2026: Setting Up Turborepo and Nx for Modern JavaScript Projects

Monorepos — single repositories containing multiple related packages or applications — have become the standard architecture for medium and large JavaScript projects. Companies like Google, Meta, Vercel, and Microsoft manage enormous codebases in monorepos. The tooling in 2026 has matured dramatically: Turborepo and Nx are the two leading solutions, each with a distinct philosophy. This guide covers both tools, helps you choose between them, and provides practical setup instructions for real-world project structures.

Why Use a Monorepo?

Before picking a tool, understand the benefits and trade-offs that make monorepos compelling.

  • Atomic cross-package changes — Change a shared utility and the consuming apps in one commit, with one PR
  • Shared tooling — One ESLint config, one TypeScript base config, one testing setup
  • Simplified dependency management — No version mismatch between internal packages
  • Faster CI with caching — Only rebuild and retest what actually changed
  • Code sharing without publishing — Share code between apps without npm publish cycles
  • Unified developer experience — One place to clone, one npm install, consistent commands
FeatureTurborepoNx
Learning curveLowMedium-High
Config overheadMinimalModerate
Build cachingExcellent (local + remote)Excellent (local + remote)
Task orchestrationPipeline-basedGraph-based (more powerful)
Code generationBasic generatorsFull generator system
IDE integrationGoodExcellent (Nx Console)
Best forFrontend/fullstack, Next.js shopsEnterprise, Angular, mixed stacks

Turborepo Setup

Turborepo (from Vercel) is the simpler of the two. It focuses on task running and caching with minimal configuration. It integrates naturally with npm/yarn/pnpm workspaces.

Initial Setup

# Create a new Turborepo project
npx create-turbo@latest my-monorepo

# Or add Turborepo to an existing workspace
npm install turbo --save-dev --workspace-root

# Recommended: use pnpm for best workspace performance
pnpm dlx create-turbo@latest my-monorepo --package-manager pnpm

Project Structure

my-monorepo/
├── apps/
│   ├── web/                    # Next.js app
│   │   ├── package.json
│   │   └── ...
│   ├── admin/                  # Another Next.js app
│   │   ├── package.json
│   │   └── ...
│   └── api/                    # Express/Fastify API
│       ├── package.json
│       └── ...
├── packages/
│   ├── ui/                     # Shared React component library
│   │   ├── package.json        # name: "@acme/ui"
│   │   ├── src/
│   │   │   ├── Button.tsx
│   │   │   ├── Card.tsx
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── config/                 # Shared configs
│   │   ├── package.json        # name: "@acme/config"
│   │   ├── eslint/
│   │   │   └── index.js
│   │   └── typescript/
│   │       ├── base.json
│   │       ├── nextjs.json
│   │       └── react-library.json
│   └── utils/                  # Shared utilities
│       ├── package.json        # name: "@acme/utils"
│       └── src/
│           ├── format.ts
│           └── validate.ts
├── turbo.json                  # Turborepo config
├── package.json                # Root workspace config
└── pnpm-workspace.yaml

turbo.json Configuration

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
      "cache": true
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "lint": {
      "outputs": [],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "*.json", ".eslintrc.*"]
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "clean": {
      "cache": false
    }
  }
}

The ^build syntax means "run build in all dependencies first." This ensures @acme/ui is built before the apps that depend on it.

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "type-check": "turbo run type-check",
    "clean": "turbo run clean && rm -rf node_modules",
    "format": "prettier --write "**/*.{ts,tsx,md,json}" --ignore-path .gitignore"
  },
  "devDependencies": {
    "turbo": "latest",
    "prettier": "^3.0.0",
    "typescript": "^5.4.0"
  },
  "packageManager": "pnpm@9.0.0"
}

pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'packages/*'

Shared TypeScript Configuration

// packages/config/typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "exactOptionalPropertyTypes": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

// packages/config/typescript/nextjs.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "ES2022"],
    "allowJs": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  }
}

Internal Package Setup

// packages/ui/package.json
{
  "name": "@acme/ui",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts"
    }
  },
  "scripts": {
    "lint": "eslint src/ --ext .ts,.tsx",
    "type-check": "tsc --noEmit"
  },
  "devDependencies": {
    "@acme/config": "workspace:*",
    "@types/react": "^18.3.0",
    "typescript": "^5.4.0"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  }
}
// packages/ui/src/Button.tsx
import { type ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export function Button({
  variant = 'primary',
  size = 'md',
  isLoading = false,
  children,
  disabled,
  className = '',
  ...props
}: ButtonProps) {
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
    danger: 'bg-red-600 text-white hover:bg-red-700',
  };

  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  };

  return (
    <button
      {...props}
      disabled={disabled || isLoading}
      className={`${variantClasses[variant]} ${sizeClasses[size]} rounded-lg font-medium transition-colors ${className}`}
    >
      {isLoading ? 'Loading...' : children}
    </button>
  );
}

// packages/ui/src/index.ts — barrel export
export { Button } from './Button';
export { Card } from './Card';
export type { ButtonProps } from './Button';

Remote Caching

Remote caching is one of Turborepo's most powerful features. Build artifacts are shared across all developers and CI machines, so if a colleague already built the same commit, you get an instant cache hit instead of rebuilding.

# Vercel Remote Cache (free for Vercel users)
npx turbo login
npx turbo link

# Self-hosted remote cache (Turborepo supports any S3-compatible store)
# Set these environment variables:
# TURBO_API=https://your-cache-server.com
# TURBO_TOKEN=your-token
# TURBO_TEAM=your-team-slug

# GitHub Actions with remote cache
# .github/workflows/ci.yml
# env:
#   TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
#   TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Nx Setup

Nx provides a more structured and feature-rich monorepo experience. It shines in large teams, enterprise environments, and projects with multiple frameworks (React, Angular, Node.js, etc.).

# Create new Nx workspace
npx create-nx-workspace@latest my-workspace

# Add to existing project
npx nx@latest init

# Add Next.js app
nx generate @nx/next:application web --directory=apps/web

# Add a shared library
nx generate @nx/react:library ui --directory=packages/ui --bundler=vite

# Add an Express API
nx generate @nx/express:application api --directory=apps/api

# Run affected tasks only (most powerful Nx feature)
# Only tests packages changed since last commit
nx affected --target=test

# Visualize the project dependency graph
nx graph

nx.json Configuration

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "defaultBase": "main",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "plugins": [
    "@nx/next/plugin",
    "@nx/eslint/plugin",
    "@nx/vite/plugin"
  ]
}

CI/CD Pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for Nx affected commands

      - uses: pnpm/action-setup@v3
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # For Turborepo
      - name: Build
        run: pnpm turbo build --filter="...[origin/main]"
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Test
        run: pnpm turbo test --filter="...[origin/main]"
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      # For Nx (alternative)
      # - name: Build affected
      #   run: npx nx affected --target=build --base=origin/main
      # - name: Test affected
      #   run: npx nx affected --target=test --base=origin/main

Choosing: Turborepo vs Nx

  • Choose Turborepo if you want minimal config, are already using Vercel, your stack is primarily Next.js, or you want to get started in under an hour.
  • Choose Nx if you need code generation, have a large team that benefits from enforced project boundaries, work with multiple frameworks, or need the project graph visualization to understand dependency relationships.
  • Both are excellent choices for remote caching and affected task running, which are the core features that make monorepos fast in CI.

When working with your monorepo packages, our JSON Formatter tool can help validate package.json and turbo.json files. For generating unique IDs across your packages, try our UUID Generator. You can also read our Git Branching Strategies guide for branch management patterns that work well with monorepo workflows.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

{ }JSON Formatter📋YAML Formatter

Related Articles

Git 워크플로 전략 비교

Gitflow, GitHub Flow, 트렁크 기반 개발 비교.

Docker 보안 모범 사례: 컨테이너 하드닝 가이드

Docker 컨테이너 보안 종합 가이드 — 최소 이미지, 비root 사용자, 시크릿 관리.

CI/CD 파이프라인 모범 사례: GitHub Actions, 테스트 및 배포

GitHub Actions로 강력한 CI/CD 파이프라인 구축 — 테스트 전략과 배포 패턴.