콘텐츠 대표 이미지 - 자바스크립트 모노레포: 대규모 프로젝트 관리 전략과 실전 운영 가이드
📦 JavaScript · 프로그램개발

자바스크립트 모노레포: 대규모 프로젝트 관리 전략과 실전 운영 가이드

Monorepo 구조 설계부터 툴체인 선택, CI/CD 최적화까지 — 실무에서 바로 쓰는 전략 🚀

야 솔직히 말해서... 프로젝트가 커지면 커질수록 레포지토리 관리가 진짜 지옥이 되지 않음? 🔥
프론트엔드 레포 따로, 백엔드 레포 따로, 공통 유틸 레포 따로... 그러다 보면 어느 순간 버전 충돌이 터지고, 공통 컴포넌트 수정하면 10개 레포 다 돌아다니면서 PR 날려야 하는 그 고통 ㅠㅠ

그래서 요즘 대형 테크 기업들이 너도나도 도입하고 있는 게 바로 모노레포(Monorepo)야.
구글, 메타, 마이크로소프트, 에어비앤비... 이 친구들 다 모노레포 씀. 괜히 쓰는 게 아니거든?

이 글에서는 자바스크립트 생태계에서 모노레포를 어떻게 설계하고, 어떤 툴을 선택하고, 실제로 어떻게 운영하는지 진짜 실무 레벨로 파헤쳐볼 거야. 준비됐지? ㄱㄱ 😎

모노레포 vs 멀티레포 구조 비교 멀티레포 (Multi-repo) 📁 frontend react@18.0.0 utils@1.2.0 📁 backend express@4.18 utils@1.0.0 📁 mobile rn@0.71.0 utils@1.1.0 ⚠️ utils 버전 불일치! 동기화 지옥 시작... ❌ 중복 의존성 설치 ❌ 개별 CI/CD 관리 ❌ 크로스 레포 PR ❌ 코드 공유 어려움 관리 복잡도 📈 개발 속도 📉 모노레포 (Mono-repo) 🏠 루트 레포지토리 📦 frontend react@18.0.0 공유 utils ✓ 📦 backend express@4.18 공유 utils ✓ 📦 mobile rn@0.71.0 공유 utils ✓ 🔗 packages/utils (단일 버전 관리) ✅ 단일 의존성 관리 ✅ 통합 CI/CD 관리 효율 📈 개발 속도 📈

🤔 모노레포가 뭔데? 진짜로

모노레포(Monorepo)여러 개의 프로젝트나 패키지를 하나의 단일 레포지토리에서 관리하는 소프트웨어 개발 전략이야.
"모노(Mono) = 하나" + "레포(Repo) = 레포지토리" 합치면 그냥 하나의 레포에 다 때려넣는다는 거임 ㅋㅋ

근데 여기서 중요한 거! 모노레포는 모놀리식(Monolithic) 아키텍처랑 완전히 다른 개념이야.
모놀리식은 코드 자체가 하나로 뭉쳐있는 거고, 모노레포는 코드는 패키지별로 분리되어 있지만 레포지토리만 하나인 거거든.

📌 핵심 개념 정리

모노레포 = 하나의 레포지토리 + 여러 개의 독립적인 패키지/앱
멀티레포 = 여러 개의 레포지토리 + 각각의 패키지/앱
모놀리식 = 하나의 레포지토리 + 하나의 거대한 코드베이스 (이건 다른 얘기!)

모노레포를 쓰는 진짜 이유

단순히 "레포 하나로 합치면 편하겠지~" 이런 생각으로 쓰는 게 아님.
실제로 모노레포가 해결해주는 문제들이 있어:

🔄 코드 공유 & 재사용
공통 유틸, 타입 정의, UI 컴포넌트를 여러 앱에서 즉시 공유 가능. npm 배포 없이도 로컬 참조로 바로 사용!
📦 의존성 통합 관리
루트 레벨에서 의존성을 한 번에 관리. 버전 충돌 문제가 대폭 줄어들고 중복 설치도 방지됨.
🔀 원자적 커밋
여러 패키지에 걸친 변경사항을 하나의 커밋으로 처리. "이 PR이 어떤 패키지에 영향 주는지" 한눈에 파악!
🛠️ 통합 툴링
ESLint, Prettier, TypeScript 설정을 루트에서 한 번만 하면 모든 패키지에 적용. 설정 지옥 탈출!
💡 알아두면 좋은 사실

구글은 수십억 줄의 코드를 단 하나의 모노레포에서 관리한다고 알려져 있어. 물론 우리가 구글 수준의 인프라를 갖출 순 없지만, 그 철학은 충분히 가져올 수 있음! 재능넷 같은 플랫폼에서도 프론트엔드, 백엔드, 공통 타입을 모노레포로 관리하면 개발 효율이 확 올라가거든.

🛠️ 모노레포 툴 전쟁: 뭘 써야 해?

자바스크립트 생태계에서 모노레포를 지원하는 툴이 진짜 많아.
근데 다 비슷비슷해 보여서 뭘 골라야 할지 모르겠다고? 걱정 마, 지금 다 정리해줄게 ㅋㅋ

패키지 매니저 레벨: 워크스페이스(Workspace)

모노레포의 기본은 워크스페이스(Workspace) 기능이야.
npm, yarn, pnpm 모두 워크스페이스를 지원하는데, 각각 특성이 달라.

📋 npm workspaces (package.json)

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}

📋 pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'apps/*'
  - '!**/test/**'
npm workspaces
npm 7+부터 기본 지원. 별도 설치 불필요. 심볼릭 링크로 패키지 연결. 속도는 좀 느린 편.
기본 내장 속도 보통
yarn workspaces
Yarn Classic(v1)부터 지원. Yarn Berry(v2+)는 PnP 방식으로 더 강력. 생태계 성숙도 높음.
성숙한 생태계 PnP 지원
pnpm workspaces
하드링크 방식으로 디스크 공간 절약. 속도 가장 빠름. 엄격한 의존성 관리. 최근 가장 인기!
속도 최강 디스크 절약
💡 2024년 기준 추천

pnpm이 현재 모노레포 환경에서 가장 많이 선택받고 있어. 속도도 빠르고, 디스크 공간도 절약되고, 의존성 관리도 엄격해서 "유령 의존성(phantom dependency)" 문제를 원천 차단함. 새 프로젝트라면 pnpm 강추!

빌드 오케스트레이션 레벨: Turborepo vs Nx

워크스페이스만으로는 부족해. 패키지가 많아지면 빌드 순서 관리, 캐싱, 병렬 실행이 필요해지거든.
이걸 해결해주는 게 빌드 오케스트레이션 툴이야.

빌드 오케스트레이션 툴 비교 ⚡ Turborepo by Vercel 🚀 원격 캐싱 (Remote Caching) Vercel 클라우드 캐시 공유로 팀 전체 빌드 속도 향상 📊 태스크 파이프라인 turbo.json으로 태스크 의존성 그래프 정의, 병렬 실행 🎯 Zero Config 철학 최소한의 설정으로 빠르게 시작, 학습 곡선 낮음 🔧 점진적 도입 가능 기존 프로젝트에 turbo.json 하나만 추가하면 바로 적용 💡 이런 팀에 추천 • 빠르게 모노레포 도입하고 싶은 팀 • Next.js / Vercel 스택 사용 팀 • 소~중규모 프로젝트 🔷 Nx by Nrwl 🧠 프로젝트 그래프 분석 의존성 그래프 시각화, 영향받는 패키지만 선택적 빌드 🔌 강력한 플러그인 생태계 React, Angular, Node, Next.js 등 공식 플러그인 다수 ⚙️ 코드 생성기 (Generators) nx generate로 표준화된 코드 스캐폴딩 자동화 📈 엔터프라이즈 기능 분산 태스크 실행, 고급 캐싱, 팀 협업 기능 강화 💡 이런 팀에 추천 • 대규모 엔터프라이즈 프로젝트 • 다양한 프레임워크 혼용 팀 • 코드 표준화가 중요한 팀

Lerna는 어떻게 됐어?

예전에 모노레포 하면 무조건 Lerna 얘기가 나왔는데... 지금은 좀 달라졌어.
Lerna는 한동안 유지보수가 멈췄다가 Nx 팀이 인수해서 다시 살아났어. 지금은 Lerna + Nx 조합으로 쓰는 경우도 있음.

⚠️ 주의사항

Lerna 단독으로 새 프로젝트 시작하는 건 2024년 기준으로 추천하지 않아. Turborepo나 Nx를 메인으로 쓰고, 필요하다면 버전 관리/배포 자동화 목적으로만 Lerna를 보조로 쓰는 게 나음.

Changesets: 버전 관리의 구원자

모노레포에서 패키지 버전 관리는 진짜 골치 아픈 문제야.
Changesets는 이 문제를 우아하게 해결해주는 툴인데, 아직 모르는 사람이 많더라고.

Changesets 동작 방식

1
changeset 생성: pnpm changeset 실행 → 어떤 패키지가 변경됐는지, 변경 타입(major/minor/patch)이 뭔지 선택
2
.changeset 폴더에 마크다운 파일 생성: 변경 내용이 자동으로 기록됨. 이걸 PR에 포함시켜서 리뷰받음
3
버전 업데이트: pnpm changeset version 실행 → package.json 버전 자동 업데이트 + CHANGELOG.md 자동 생성
4
배포: pnpm changeset publish 실행 → npm에 자동 배포

🏗️ 실전 모노레포 구조 설계하기

이론은 충분히 봤으니까 이제 진짜 실전으로 가보자.
어떻게 폴더 구조를 잡아야 하는지, 어떤 패턴이 좋은지 알아볼게.

기본 폴더 구조

📁 권장 모노레포 폴더 구조

my-monorepo/
├── apps/                    # 실제 배포되는 앱들
│   ├── web/                 # Next.js 웹 앱
│   ├── mobile/              # React Native 앱
│   └── admin/               # 관리자 대시보드
├── packages/                # 공유 패키지들
│   ├── ui/                  # 공통 UI 컴포넌트
│   ├── utils/               # 공통 유틸리티 함수
│   ├── types/               # 공통 TypeScript 타입
│   ├── config/              # 공통 설정 (ESLint, TS 등)
│   └── api-client/          # API 클라이언트
├── tooling/                 # 개발 도구 설정
│   ├── eslint-config/       # 공통 ESLint 설정
│   └── tsconfig/            # 공통 TypeScript 설정
├── package.json             # 루트 package.json
├── pnpm-workspace.yaml      # pnpm 워크스페이스 설정
├── turbo.json               # Turborepo 설정
└── .changeset/              # Changesets 설정
💡 apps vs packages 구분 기준

apps/: 최종 사용자에게 배포되는 앱. 다른 패키지에서 import하지 않음.
packages/: 다른 앱이나 패키지에서 import해서 쓰는 공유 라이브러리.
이 구분을 명확히 해야 의존성 방향이 꼬이지 않아!

루트 package.json 설정

📋 루트 package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "type-check": "turbo run type-check",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.0.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=8.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

turbo.json 설정 (핵심!)

Turborepo의 핵심은 태스크 파이프라인이야.
어떤 태스크가 어떤 태스크에 의존하는지 정의해서 올바른 순서로 실행되게 함.

📋 turbo.json

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

🔑 turbo.json 핵심 개념

"dependsOn": ["^build"]: ^ 기호는 "의존하는 패키지들의 build가 먼저 완료되어야 함"을 의미
"cache": true: 출력물을 캐싱해서 변경이 없으면 재실행 안 함 (속도 핵심!)
"persistent": true: dev 서버처럼 종료되지 않고 계속 실행되는 태스크
"outputs": 캐싱할 파일/폴더 패턴 지정

공유 패키지 만들기 (packages/ui 예시)

📋 packages/ui/package.json

{
  "name": "@my-monorepo/ui",
  "version": "0.1.0",
  "private": false,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@my-monorepo/eslint-config": "workspace:*",
    "@my-monorepo/tsconfig": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}
💡 workspace:* 이게 뭐야?

workspace:*는 pnpm의 워크스페이스 프로토콜이야. npm 레지스트리에서 패키지를 가져오는 게 아니라 로컬 워크스페이스에 있는 패키지를 직접 참조하는 거임. 이 덕분에 공유 패키지를 수정하면 즉시 반영돼서 개발할 때 엄청 편해!

📘 TypeScript 설정 공유 전략

모노레포에서 TypeScript 설정을 각 패키지마다 따로 하면 관리가 지옥이 돼.
기본 설정을 공유하고 각 패키지에서 확장(extends)하는 패턴을 써야 해.

tooling/tsconfig 패키지 구성

📋 tooling/tsconfig/base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "bundler",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "exclude": ["node_modules"]
}

📋 tooling/tsconfig/nextjs.json (Next.js 앱용)

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Next.js",
  "extends": "./base.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowJs": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"]
  }
}

📋 apps/web/tsconfig.json (실제 앱에서 사용)

{
  "extends": "@my-monorepo/tsconfig/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

ESLint 설정 공유

📋 tooling/eslint-config/index.js

/** @type {import("eslint").Linter.Config} */
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:import/recommended",
    "plugin:import/typescript",
    "prettier"
  ],
  plugins: ["@typescript-eslint", "import"],
  parser: "@typescript-eslint/parser",
  rules: {
    "@typescript-eslint/no-unused-vars": [
      "error",
      { argsIgnorePattern: "^_" }
    ],
    "import/order": [
      "error",
      {
        "groups": [
          "builtin", "external", "internal",
          "parent", "sibling", "index"
        ],
        "newlines-between": "always",
        "alphabetize": { "order": "asc" }
      }
    ]
  }
};

⚡ 캐싱 전략: 빌드 시간을 10분의 1로 줄이는 법

모노레포의 진짜 파워는 캐싱에서 나와.
변경된 패키지만 다시 빌드하고, 변경 없는 건 캐시에서 가져오면 빌드 시간이 드라마틱하게 줄어들거든.

로컬 캐싱 (기본)

Turborepo는 기본적으로 .turbo 폴더에 로컬 캐시를 저장해.
같은 입력(소스코드 + 환경변수)이면 이전 출력을 그대로 재사용함.

캐시 히트 예시

$ turbo run build

• Packages in scope: web, mobile, ui, utils
• Running build in 4 packages

@my-monorepo/utils:build: cache hit, replaying output ⚡
@my-monorepo/ui:build: cache hit, replaying output ⚡
@my-monorepo/web:build: cache miss, executing...
@my-monorepo/mobile:build: cache miss, executing...

 Tasks:    4 successful, 4 total
Cached:    2 cached, 4 total
  Time:    8.234s >>> FULL TURBO 2.1s

utils랑 ui는 변경 없으니까 캐시 히트! web이랑 mobile만 새로 빌드함 ㄷㄷ

원격 캐싱 (Remote Caching)

로컬 캐시는 내 컴퓨터에만 있어서 팀원들이나 CI 서버는 혜택을 못 받아.
원격 캐싱을 설정하면 팀 전체가 캐시를 공유할 수 있어!

🔴 Vercel Remote Cache
Turborepo 공식 원격 캐시. Vercel 계정만 있으면 무료로 사용 가능. 설정이 매우 간단함.
무료간편 설정
🔷 Self-hosted Cache
Turborepo Remote Cache API를 직접 구현하거나 오픈소스 솔루션(ducktape 등) 사용. 완전한 데이터 통제 가능.
설정 복잡데이터 통제

📋 Vercel Remote Cache 설정

# 1. Vercel 로그인
npx turbo login

# 2. 레포지토리 연결
npx turbo link

# 3. 이제 자동으로 원격 캐시 사용!
# CI에서는 환경변수로 설정
TURBO_TOKEN=your_token
TURBO_TEAM=your_team_name
💡 실제 효과는?

팀원 A가 빌드한 결과물을 팀원 B가 그대로 가져다 씀. CI 서버도 마찬가지. 실제로 원격 캐싱 도입 후 CI 빌드 시간이 10분 → 1분 30초로 줄었다는 사례도 있어. 이게 진짜 모노레포의 힘임 ㄷㄷ

Nx의 캐싱 전략

Nx도 비슷한 캐싱 시스템을 가지고 있어. Nx Cloud를 통해 원격 캐싱과 분산 태스크 실행을 지원함.

📋 nx.json 캐싱 설정

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "lint", "test", "type-check"],
        "parallel": 3
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    }
  }
}
Turborepo 캐싱 동작 원리 📥 입력 수집 소스코드 + 환경변수 + 의존성 버전 🔑 해시 생성 SHA-256 해시값 ab3f9c2d... 🗄️ 캐시 조회 로컬 / 원격 캐시 해시값으로 검색 ✅ 캐시 히트! 저장된 출력물 즉시 복원 빌드 실행 없이 완료! ⚡ 수초 내 완료 ⚠️ 캐시 미스 태스크 실제 실행 출력물 생성 후 캐시 저장 다음번엔 히트됨 😊 해시 일치 해시 불일치 ☁️ 원격 캐시 (Vercel / Self-hosted): 팀 전체가 캐시 공유 → CI 빌드 시간 대폭 단축!

🔄 CI/CD 파이프라인 최적화

모노레포에서 CI/CD를 잘못 설정하면 오히려 더 느려질 수 있어.
변경된 패키지만 빌드/테스트하는 게 핵심이야.

GitHub Actions 설정 예시

📋 .github/workflows/ci.yml

name: CI

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

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
  build:
    name: Build & Test
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 전체 히스토리 필요 (변경 감지용)
      
      - name: Setup pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 8
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Type check
        run: pnpm turbo run type-check
      
      - name: Lint
        run: pnpm turbo run lint
      
      - name: Build
        run: pnpm turbo run build
      
      - name: Test
        run: pnpm turbo run test

영향받는 패키지만 배포하기

모노레포에서 배포할 때 모든 앱을 다 배포하면 낭비야.
변경된 패키지에 의존하는 앱만 선택적으로 배포해야 해.

📋 변경된 패키지 감지 스크립트

# Turborepo로 변경된 패키지 목록 확인
turbo run build --dry=json | jq '.packages'

# 또는 git diff로 직접 확인
git diff --name-only HEAD~1 HEAD | \
  grep -E '^(apps|packages)/' | \
  cut -d'/' -f1-2 | \
  sort -u

📋 선택적 배포 GitHub Actions

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      web: ${{ steps.changes.outputs.web }}
      mobile: ${{ steps.changes.outputs.mobile }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: changes
        with:
          filters: |
            web:
              - 'apps/web/**'
              - 'packages/ui/**'
              - 'packages/utils/**'
            mobile:
              - 'apps/mobile/**'
              - 'packages/utils/**'

  deploy-web:
    needs: detect-changes
    if: needs.detect-changes.outputs.web == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy Web App
        run: echo "웹 앱 배포!"
        # 실제 배포 명령어

  deploy-mobile:
    needs: detect-changes
    if: needs.detect-changes.outputs.mobile == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy Mobile App
        run: echo "모바일 앱 배포!"
💡 Turborepo의 --filter 옵션

turbo run build --filter=web...: web 앱과 web이 의존하는 모든 패키지 빌드
turbo run build --filter=[HEAD^1]: 이전 커밋 대비 변경된 패키지만 빌드
turbo run build --filter=!mobile: mobile 제외하고 빌드
이 필터 기능이 진짜 강력해서 CI 시간을 엄청 줄여줌!

✅ 실전 패턴 vs ❌ 안티패턴

모노레포를 도입하면서 많은 팀들이 비슷한 실수를 반복해.
미리 알고 피하면 나중에 고생 안 해도 됨 ㅋㅋ

패키지 의존성 방향

❌ 안티패턴: 순환 의존성
packages/ui → packages/utils
packages/utils → packages/ui
# 이러면 빌드 순서 결정 불가!
# 무한 루프 발생 ㅠㅠ
✅ 올바른 패턴: 단방향 의존성
apps/* → packages/ui
apps/* → packages/utils
packages/ui → packages/utils
# 의존성이 항상 한 방향!
# 빌드 순서 명확하게 결정됨

공유 패키지 설계

❌ 안티패턴: 거대한 단일 패키지
packages/
└── shared/
    ├── components/  # UI 컴포넌트
    ├── utils/       # 유틸리티
    ├── types/       # 타입
    ├── hooks/       # 훅
    └── api/         # API 클라이언트
# 하나 바뀌면 전체 재빌드!
# 트리쉐이킹도 어려움
✅ 올바른 패턴: 목적별 분리
packages/
├── ui/          # UI 컴포넌트만
├── utils/       # 유틸리티만
├── types/       # 타입만
├── hooks/       # 훅만
└── api-client/  # API 클라이언트만
# 변경된 패키지만 재빌드!
# 명확한 책임 분리

버전 관리 전략

❌ 안티패턴: 수동 버전 관리
# 개발자가 직접 package.json 수정
# CHANGELOG 수동 작성
# 어떤 패키지 버전 올렸는지 추적 불가
# 배포 실수 빈번하게 발생
✅ 올바른 패턴: Changesets 사용
# pnpm changeset → 변경 기록
# pnpm changeset version → 자동 버전업
# pnpm changeset publish → 자동 배포
# CHANGELOG 자동 생성
# 실수 없는 체계적 관리

내부 패키지 참조 방식

❌ 안티패턴: 상대 경로 직접 참조
// apps/web/src/components/Button.tsx
import { cn } from '../../../packages/utils/src/cn'
// 경로가 깨지기 쉽고 리팩토링 어려움
// IDE 자동완성도 잘 안 됨
✅ 올바른 패턴: 패키지 이름으로 참조
// apps/web/src/components/Button.tsx
import { cn } from '@my-monorepo/utils'
// 패키지 이름으로 깔끔하게 참조
// IDE 자동완성 완벽 지원
// 리팩토링도 쉬움
⚠️ 모노레포 도입 전 체크리스트

✔️ 팀 규모가 최소 3명 이상인가? (너무 작은 팀엔 오버엔지니어링일 수 있음)
✔️ 실제로 공유할 코드가 있는가? (공유 코드 없으면 모노레포 이점이 없음)
✔️ CI/CD 인프라를 개선할 준비가 됐는가?
✔️ 팀원들이 모노레포 개념을 이해하고 있는가?
✔️ 초기 설정에 투자할 시간이 있는가? (처음 세팅이 좀 걸림)

🚀 처음부터 모노레포 세팅하기 (Step by Step)

이론 다 배웠으니까 이제 실제로 처음부터 세팅하는 과정을 따라가보자.
pnpm + Turborepo 조합으로 갈게!

1
프로젝트 초기화
TERMINAL
mkdir my-monorepo && cd my-monorepo
pnpm init
# package.json에 "private": true 추가 (필수!)

# Turborepo 설치
pnpm add -D turbo -w
2
워크스페이스 설정
pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tooling/*'
3
폴더 구조 생성
TERMINAL
mkdir -p apps/web apps/admin
mkdir -p packages/ui packages/utils packages/types
mkdir -p tooling/eslint-config tooling/tsconfig
4
공유 TypeScript 설정 생성
tooling/tsconfig/package.json
{
  "name": "@my-monorepo/tsconfig",
  "version": "0.0.0",
  "private": true,
  "files": ["base.json", "nextjs.json", "react-library.json"]
}
5
Next.js 앱 생성
TERMINAL
cd apps/web
pnpm create next-app . --typescript --tailwind --app
# 생성 후 package.json name을 "@my-monorepo/web"으로 변경
6
공유 UI 패키지 설정
packages/ui/package.json
{
  "name": "@my-monorepo/ui",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "devDependencies": {
    "@my-monorepo/tsconfig": "workspace:*",
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}
7
앱에서 공유 패키지 사용
apps/web/package.json에 추가
{
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    "@my-monorepo/utils": "workspace:*"
  }
}
TERMINAL
pnpm install  # 루트에서 실행
# 이제 apps/web에서 @my-monorepo/ui import 가능!
8
turbo.json 설정 후 실행
TERMINAL
# 전체 빌드
pnpm turbo run build

# 개발 서버 (모든 앱 동시 실행)
pnpm turbo run dev --parallel

# 특정 앱만 실행
pnpm turbo run dev --filter=web
💡 빠른 시작 팁

처음부터 다 설정하기 귀찮으면 npx create-turbo@latest 명령어로 Turborepo 공식 템플릿을 사용할 수 있어. Next.js + 공유 UI 패키지 + TypeScript 설정이 다 포함된 스타터 키트가 바로 생성됨!

🎯 고급 전략: 대규모 팀을 위한 운영 노하우

기본 세팅은 됐고, 이제 팀이 커지고 프로젝트가 복잡해졌을 때 어떻게 운영하는지 알아보자.

패키지 소유권 관리 (CODEOWNERS)

모노레포에서 여러 팀이 작업하면 누가 어떤 패키지를 담당하는지 명확히 해야 해.
GitHub의 CODEOWNERS 파일로 이걸 자동화할 수 있어.

📋 .github/CODEOWNERS

# 전체 레포 기본 소유자
*                           @org/platform-team

# 앱별 소유자
apps/web/**                 @org/frontend-team
apps/mobile/**              @org/mobile-team
apps/admin/**               @org/admin-team

# 공유 패키지 소유자
packages/ui/**              @org/design-system-team
packages/utils/**           @org/platform-team
packages/api-client/**      @org/backend-team

# 인프라 설정
.github/**                  @org/devops-team
turbo.json                  @org/devops-team

패키지 경계 강제하기

모노레포가 커지면 패키지 간 의존성이 엉키기 시작해.
ESLint의 import 규칙으로 허용되지 않은 의존성을 자동으로 차단할 수 있어.

📋 eslint-plugin-boundaries 설정

// .eslintrc.js
module.exports = {
  plugins: ["boundaries"],
  settings: {
    "boundaries/elements": [
      { type: "app", pattern: "apps/*" },
      { type: "ui", pattern: "packages/ui" },
      { type: "utils", pattern: "packages/utils" },
      { type: "types", pattern: "packages/types" }
    ]
  },
  rules: {
    "boundaries/element-types": [
      "error",
      {
        default: "disallow",
        rules: [
          // app은 모든 패키지 사용 가능
          { from: "app", allow: ["ui", "utils", "types"] },
          // ui는 utils와 types만 사용 가능
          { from: "ui", allow: ["utils", "types"] },
          // utils는 types만 사용 가능
          { from: "utils", allow: ["types"] },
          // types는 아무것도 import 안 함
          { from: "types", allow: [] }
        ]
      }
    ]
  }
};

모노레포에서의 테스트 전략

🧪 단위 테스트
각 패키지 내부에서 독립적으로 실행. Vitest 또는 Jest 사용. 캐싱 효과 최대화.
🔗 통합 테스트
여러 패키지가 함께 동작하는지 테스트. 앱 레벨에서 실행. 변경된 앱만 선택적 실행.
🎭 E2E 테스트
Playwright 또는 Cypress로 실제 사용자 시나리오 테스트. 배포 전 최종 검증.
📸 시각적 회귀 테스트
UI 패키지 변경 시 Storybook + Chromatic으로 시각적 변화 자동 감지.

📋 루트 Jest 설정 (jest.config.ts)

import type { Config } from 'jest';

const config: Config = {
  projects: [
    '<rootDir>/packages/*/jest.config.ts',
    '<rootDir>/apps/*/jest.config.ts'
  ],
  coverageDirectory: '<rootDir>/coverage',
  collectCoverageFrom: [
    '<rootDir>/packages/*/src/**/*.{ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**'
  ]
};

export default config;

모노레포 마이그레이션 전략

기존 멀티레포에서 모노레포로 이전할 때 한 번에 다 옮기려고 하면 망해.
점진적 마이그레이션이 핵심이야.

1
파일럿 패키지 선택: 가장 많이 공유되는 유틸리티 패키지 하나를 먼저 모노레포로 이전
2
인프라 검증: CI/CD, 캐싱, 배포 파이프라인이 제대로 동작하는지 확인
3
팀 교육: 모노레포 워크플로우, 커밋 컨벤션, Changesets 사용법 교육
4
순차적 이전: 의존성이 적은 패키지부터 순서대로 이전. 한 번에 하나씩!
5
레거시 레포 아카이브: 모든 이전 완료 후 기존 레포를 읽기 전용으로 아카이브
💡 git history 보존하기

기존 레포의 git 히스토리를 모노레포로 가져올 때 git subtree 또는 git filter-repo를 사용하면 커밋 히스토리를 보존할 수 있어. 히스토리 날리면 나중에 blame 추적할 때 고생하거든!

⚡ 성능 최적화: 빌드 속도를 극한까지 끌어올리기

pnpm의 하드링크 방식 이해하기

pnpm이 npm/yarn보다 빠른 이유는 하드링크(Hard Link) 방식 때문이야.
같은 패키지를 여러 프로젝트에서 쓸 때 파일을 복사하지 않고 하드링크로 연결함.

npm vs pnpm 디스크 사용량 비교

npm/yarn: 각 프로젝트마다 node_modules에 패키지 복사 → 10개 프로젝트면 같은 패키지가 10번 복사됨
pnpm: 글로벌 스토어에 한 번만 저장 + 하드링크로 연결 → 디스크 공간 60~80% 절약!
모노레포에서 패키지가 많아질수록 이 차이가 더 커짐

병렬 실행 최적화

📋 병렬 실행 설정

# turbo.json - 동시 실행 수 제한
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"]
    }
  }
}

# 실행 시 병렬 수 지정
turbo run build --concurrency=10

# CPU 코어 수 기반 자동 설정
turbo run build --concurrency=50%

tsup으로 패키지 빌드 최적화

tsup은 esbuild 기반의 TypeScript 번들러로, 공유 패키지 빌드에 최적화되어 있어.
설정이 거의 필요 없고 CJS/ESM 동시 출력, 타입 선언 파일 생성을 자동으로 해줌.

📋 packages/utils/tsup.config.ts

import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  treeshake: true,
  minify: false,  // 라이브러리는 보통 minify 안 함
  external: ['react', 'react-dom'],  // peer deps는 번들에서 제외
});

TypeScript Project References

대규모 모노레포에서 TypeScript 타입 체크 속도를 높이려면 Project References를 활용해야 해.

📋 루트 tsconfig.json (Project References 사용)

{
  "files": [],
  "references": [
    { "path": "packages/types" },
    { "path": "packages/utils" },
    { "path": "packages/ui" },
    { "path": "apps/web" },
    { "path": "apps/admin" }
  ]
}

// 각 패키지의 tsconfig.json에 composite: true 추가
{
  "compilerOptions": {
    "composite": true,
    "declarationMap": true
  }
}
💡 Project References의 장점

tsc --build 명령어로 변경된 패키지만 선택적으로 타입 체크 가능. 전체 타입 체크 시간이 대폭 줄어들어. 특히 패키지가 10개 이상 넘어가면 체감이 확 됨!

🌍 실제 기업들의 모노레포 사례

이론만 봤으니까 실제로 어떻게 쓰는지 사례도 보자.

🔵 Meta (Facebook)
React, Jest, Docusaurus 등 수십 개의 오픈소스 패키지를 단일 모노레포에서 관리. Yarn Workspaces + 자체 빌드 시스템 사용.
🟢 Vercel
Next.js 자체가 모노레포로 관리됨. Turborepo를 직접 만들어서 사용하는 만큼 가장 최적화된 모노레포 운영.
🟠 Babel
@babel/core, @babel/parser 등 100개 이상의 패키지를 모노레포로 관리. Lerna의 대표적인 성공 사례.
🔴 Jest
jest-circus, jest-runner 등 여러 패키지를 모노레포로 관리. 각 패키지가 독립적으로 배포되면서도 함께 개발됨.

이런 대형 오픈소스 프로젝트들이 모노레포를 선택한 건 이유가 있어.
패키지 간 일관성 유지, 원자적 변경, 개발 경험 향상이 그 이유야.

실제로 개발 프리랜서나 소규모 팀도 모노레포의 혜택을 충분히 누릴 수 있어.
재능넷에서 활동하는 개발자들도 클라이언트 프로젝트를 모노레포로 구성하면 유지보수 효율이 확 올라가거든. 프론트엔드, 백엔드, 공통 타입을 하나의 레포에서 관리하면 클라이언트에게 납품할 때도 훨씬 깔끔하게 전달할 수 있음!

자바스크립트 모노레포 생태계 맵 모노레포 Monorepo 📦 패키지 매니저 pnpm workspaces yarn workspaces npm workspaces ⚡ 빌드 오케스트레이션 Turborepo / Nx / Lerna 캐싱 · 병렬 실행 · 파이프라인 원격 캐시 (Vercel / Nx Cloud) 🏷️ 버전 관리 Changesets Semantic Versioning CHANGELOG 자동화 🔍 코드 품질 ESLint (공유 설정) Prettier TypeScript (공유 tsconfig) 🧪 테스트 Vitest / Jest (단위 테스트) Playwright (E2E) Storybook + Chromatic 🔄 CI/CD GitHub Actions 선택적 빌드/배포 CODEOWNERS 🔧 빌드 도구 tsup / esbuild Vite / Rollup 🚀 배포 Vercel / Netlify npm Registry 모노레포를 중심으로 연결된 자바스크립트 생태계의 주요 도구들

🔧 자주 만나는 문제와 해결법

모노레포 운영하다 보면 이런 문제들을 꼭 만나게 돼.
미리 알아두면 당황하지 않을 수 있음 ㅋㅋ

문제 1: 유령 의존성 (Phantom Dependency)

⚠️ 증상

package.json에 명시하지 않은 패키지를 import했는데 동작함. 나중에 의존성 트리가 바뀌면 갑자기 에러 발생!

해결법

pnpm을 사용하면 기본적으로 유령 의존성이 차단됨. npm/yarn을 써야 한다면 .npmrcshamefully-hoist=false 설정 추가.

문제 2: 캐시가 무효화되지 않는 문제

⚠️ 증상

코드를 변경했는데 캐시 히트가 되어서 변경사항이 반영 안 됨.

해결법

# 캐시 강제 무효화
turbo run build --force

# 특정 패키지만 캐시 무효화
turbo run build --filter=ui --force

# 로컬 캐시 전체 삭제
rm -rf .turbo

근본 원인은 turbo.json의 inputs 설정이 잘못된 경우가 많아. 환경변수나 설정 파일이 inputs에 포함되어 있는지 확인해봐.

문제 3: 순환 의존성 에러

⚠️ 증상

Error: Cycle detected in task graph 에러 발생

해결법

# 의존성 그래프 시각화로 순환 찾기
turbo run build --graph

# Nx의 경우
nx graph

그래프를 보면서 순환이 어디서 발생하는지 찾아서 의존성 방향을 수정해야 해. 보통 공통 타입을 별도 패키지로 분리하면 해결됨.

문제 4: TypeScript 경로 해석 문제

⚠️ 증상

IDE에서는 타입이 잘 보이는데 빌드하면 에러 발생. 또는 반대로 빌드는 되는데 IDE 자동완성이 안 됨.

해결법

// package.json의 exports 필드와 types 필드 확인
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"  // 이게 있어야 함!
    }
  },
  "types": "./dist/index.d.ts"  // 레거시 호환성
}

// 개발 중에는 dist 대신 src 직접 참조
{
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}

문제 5: pnpm install이 너무 느림

해결법

# .npmrc 설정으로 최적화
# .npmrc
store-dir=~/.pnpm-store
prefer-frozen-lockfile=true
strict-peer-dependencies=false

# CI에서는 frozen-lockfile 사용
pnpm install --frozen-lockfile

# 캐시 활용
- name: Cache pnpm store
  uses: actions/cache@v4
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

📋 핵심 요약 및 선택 가이드

자 이제 진짜 마무리야. 여기까지 읽었으면 모노레포에 대해 꽤 많이 알게 됐을 거야 ㅋㅋ
마지막으로 상황별 선택 가이드를 정리해줄게.

🎯 상황별 모노레포 툴 선택 가이드
빠르게 시작하고 싶다면: pnpm workspaces + Turborepo. npx create-turbo@latest로 5분 안에 세팅 완료!
🏢 대규모 엔터프라이즈라면: pnpm workspaces + Nx. 강력한 플러그인과 코드 생성기로 팀 표준화.
📦 오픈소스 라이브러리 관리라면: pnpm workspaces + Changesets. 버전 관리와 배포 자동화가 핵심.
🔄 기존 멀티레포 마이그레이션이라면: 점진적 도입. 공유 패키지 하나부터 시작해서 순차적으로 이전.
💰 비용이 중요하다면: Turborepo + Vercel Remote Cache (무료). 또는 Self-hosted 캐시 서버.

모노레포 도입 체크리스트 최종판

🏁 시작 전 확인사항

☐ Node.js 18+ 설치 확인
☐ pnpm 8+ 설치 (npm install -g pnpm)
☐ 팀원들과 모노레포 전략 합의
☐ 브랜치 전략 수립 (trunk-based vs feature branch)
☐ 커밋 컨벤션 정의 (Conventional Commits 추천)
☐ CI/CD 파이프라인 설계
☐ 원격 캐시 설정 계획
☐ 패키지 소유권 정의 (CODEOWNERS)

모노레포는 은탄환(Silver Bullet)이 아니야.
작은 팀이나 단순한 프로젝트에는 오히려 오버엔지니어링이 될 수 있어.
하지만 여러 앱이 코드를 공유하고, 팀이 함께 개발하는 환경이라면 모노레포는 진짜 게임 체인저가 될 수 있어!

재능넷에서 개발 관련 재능을 거래하거나 협업 프로젝트를 진행할 때도 이런 모노레포 구조를 미리 잡아두면 나중에 훨씬 수월하게 작업할 수 있을 거야. 처음 세팅에 시간을 좀 투자하면 나중에 몇 배로 돌아오거든 😊

💡 마지막 팁: 완벽한 설정을 처음부터 만들려고 하지 마!

모노레포 설정은 프로젝트가 성장하면서 함께 진화해야 해. 처음엔 간단하게 시작하고, 문제가 생길 때마다 개선해나가는 게 훨씬 현실적이야. 완벽한 설정을 처음부터 만들려다가 정작 개발은 못 하는 상황이 생기면 안 되잖아 ㅋㅋ

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0