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

자바스크립트 모노레포: 대규모 프로젝트 관리 전략과 실전 운영 가이드
Monorepo 구조 설계부터 툴체인 선택, CI/CD 최적화까지 — 실무에서 바로 쓰는 전략 🚀
야 솔직히 말해서... 프로젝트가 커지면 커질수록 레포지토리 관리가 진짜 지옥이 되지 않음? 🔥
프론트엔드 레포 따로, 백엔드 레포 따로, 공통 유틸 레포 따로... 그러다 보면 어느 순간 버전 충돌이 터지고, 공통 컴포넌트 수정하면 10개 레포 다 돌아다니면서 PR 날려야 하는 그 고통 ㅠㅠ
그래서 요즘 대형 테크 기업들이 너도나도 도입하고 있는 게 바로 모노레포(Monorepo)야.
구글, 메타, 마이크로소프트, 에어비앤비... 이 친구들 다 모노레포 씀. 괜히 쓰는 게 아니거든?
이 글에서는 자바스크립트 생태계에서 모노레포를 어떻게 설계하고, 어떤 툴을 선택하고, 실제로 어떻게 운영하는지 진짜 실무 레벨로 파헤쳐볼 거야. 준비됐지? ㄱㄱ 😎
🤔 모노레포가 뭔데? 진짜로
모노레포(Monorepo)는 여러 개의 프로젝트나 패키지를 하나의 단일 레포지토리에서 관리하는 소프트웨어 개발 전략이야.
"모노(Mono) = 하나" + "레포(Repo) = 레포지토리" 합치면 그냥 하나의 레포에 다 때려넣는다는 거임 ㅋㅋ
근데 여기서 중요한 거! 모노레포는 모놀리식(Monolithic) 아키텍처랑 완전히 다른 개념이야.
모놀리식은 코드 자체가 하나로 뭉쳐있는 거고, 모노레포는 코드는 패키지별로 분리되어 있지만 레포지토리만 하나인 거거든.
📌 핵심 개념 정리
모노레포 = 하나의 레포지토리 + 여러 개의 독립적인 패키지/앱
멀티레포 = 여러 개의 레포지토리 + 각각의 패키지/앱
모놀리식 = 하나의 레포지토리 + 하나의 거대한 코드베이스 (이건 다른 얘기!)
모노레포를 쓰는 진짜 이유
단순히 "레포 하나로 합치면 편하겠지~" 이런 생각으로 쓰는 게 아님.
실제로 모노레포가 해결해주는 문제들이 있어:
구글은 수십억 줄의 코드를 단 하나의 모노레포에서 관리한다고 알려져 있어. 물론 우리가 구글 수준의 인프라를 갖출 순 없지만, 그 철학은 충분히 가져올 수 있음! 재능넷 같은 플랫폼에서도 프론트엔드, 백엔드, 공통 타입을 모노레포로 관리하면 개발 효율이 확 올라가거든.
🛠️ 모노레포 툴 전쟁: 뭘 써야 해?
자바스크립트 생태계에서 모노레포를 지원하는 툴이 진짜 많아.
근데 다 비슷비슷해 보여서 뭘 골라야 할지 모르겠다고? 걱정 마, 지금 다 정리해줄게 ㅋㅋ
패키지 매니저 레벨: 워크스페이스(Workspace)
모노레포의 기본은 워크스페이스(Workspace) 기능이야.
npm, yarn, pnpm 모두 워크스페이스를 지원하는데, 각각 특성이 달라.
📋 npm workspaces (package.json)
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}
📋 pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'
pnpm이 현재 모노레포 환경에서 가장 많이 선택받고 있어. 속도도 빠르고, 디스크 공간도 절약되고, 의존성 관리도 엄격해서 "유령 의존성(phantom dependency)" 문제를 원천 차단함. 새 프로젝트라면 pnpm 강추!
빌드 오케스트레이션 레벨: Turborepo vs Nx
워크스페이스만으로는 부족해. 패키지가 많아지면 빌드 순서 관리, 캐싱, 병렬 실행이 필요해지거든.
이걸 해결해주는 게 빌드 오케스트레이션 툴이야.
Lerna는 어떻게 됐어?
예전에 모노레포 하면 무조건 Lerna 얘기가 나왔는데... 지금은 좀 달라졌어.
Lerna는 한동안 유지보수가 멈췄다가 Nx 팀이 인수해서 다시 살아났어. 지금은 Lerna + Nx 조합으로 쓰는 경우도 있음.
Lerna 단독으로 새 프로젝트 시작하는 건 2024년 기준으로 추천하지 않아. Turborepo나 Nx를 메인으로 쓰고, 필요하다면 버전 관리/배포 자동화 목적으로만 Lerna를 보조로 쓰는 게 나음.
Changesets: 버전 관리의 구원자
모노레포에서 패키지 버전 관리는 진짜 골치 아픈 문제야.
Changesets는 이 문제를 우아하게 해결해주는 툴인데, 아직 모르는 사람이 많더라고.
Changesets 동작 방식
pnpm changeset 실행 → 어떤 패키지가 변경됐는지, 변경 타입(major/minor/patch)이 뭔지 선택pnpm changeset version 실행 → package.json 버전 자동 업데이트 + CHANGELOG.md 자동 생성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/: 최종 사용자에게 배포되는 앱. 다른 패키지에서 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:*는 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 설정
# 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
}
}
}
🔄 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 "모바일 앱 배포!"
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 수동 작성
# 어떤 패키지 버전 올렸는지 추적 불가
# 배포 실수 빈번하게 발생
# 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 조합으로 갈게!
mkdir my-monorepo && cd my-monorepo
pnpm init
# package.json에 "private": true 추가 (필수!)
# Turborepo 설치
pnpm add -D turbo -w
packages:
- 'apps/*'
- 'packages/*'
- 'tooling/*'
mkdir -p apps/web apps/admin
mkdir -p packages/ui packages/utils packages/types
mkdir -p tooling/eslint-config tooling/tsconfig
{
"name": "@my-monorepo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["base.json", "nextjs.json", "react-library.json"]
}
cd apps/web
pnpm create next-app . --typescript --tailwind --app
# 생성 후 package.json name을 "@my-monorepo/web"으로 변경
{
"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"
}
}
{
"dependencies": {
"@my-monorepo/ui": "workspace:*",
"@my-monorepo/utils": "workspace:*"
}
}
pnpm install # 루트에서 실행
# 이제 apps/web에서 @my-monorepo/ui import 가능!
# 전체 빌드
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: [] }
]
}
]
}
};
모노레포에서의 테스트 전략
📋 루트 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;
모노레포 마이그레이션 전략
기존 멀티레포에서 모노레포로 이전할 때 한 번에 다 옮기려고 하면 망해.
점진적 마이그레이션이 핵심이야.
기존 레포의 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
}
}
tsc --build 명령어로 변경된 패키지만 선택적으로 타입 체크 가능. 전체 타입 체크 시간이 대폭 줄어들어. 특히 패키지가 10개 이상 넘어가면 체감이 확 됨!
🌍 실제 기업들의 모노레포 사례
이론만 봤으니까 실제로 어떻게 쓰는지 사례도 보자.
이런 대형 오픈소스 프로젝트들이 모노레포를 선택한 건 이유가 있어.
패키지 간 일관성 유지, 원자적 변경, 개발 경험 향상이 그 이유야.
실제로 개발 프리랜서나 소규모 팀도 모노레포의 혜택을 충분히 누릴 수 있어.
재능넷에서 활동하는 개발자들도 클라이언트 프로젝트를 모노레포로 구성하면 유지보수 효율이 확 올라가거든. 프론트엔드, 백엔드, 공통 타입을 하나의 레포에서 관리하면 클라이언트에게 납품할 때도 훨씬 깔끔하게 전달할 수 있음!
🔧 자주 만나는 문제와 해결법
모노레포 운영하다 보면 이런 문제들을 꼭 만나게 돼.
미리 알아두면 당황하지 않을 수 있음 ㅋㅋ
문제 1: 유령 의존성 (Phantom Dependency)
package.json에 명시하지 않은 패키지를 import했는데 동작함. 나중에 의존성 트리가 바뀌면 갑자기 에러 발생!
해결법
pnpm을 사용하면 기본적으로 유령 의존성이 차단됨. npm/yarn을 써야 한다면 .npmrc에 shamefully-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') }}
📋 핵심 요약 및 선택 가이드
자 이제 진짜 마무리야. 여기까지 읽었으면 모노레포에 대해 꽤 많이 알게 됐을 거야 ㅋㅋ
마지막으로 상황별 선택 가이드를 정리해줄게.
npx create-turbo@latest로 5분 안에 세팅 완료!
모노레포 도입 체크리스트 최종판
🏁 시작 전 확인사항
☐ Node.js 18+ 설치 확인
☐ pnpm 8+ 설치 (npm install -g pnpm)
☐ 팀원들과 모노레포 전략 합의
☐ 브랜치 전략 수립 (trunk-based vs feature branch)
☐ 커밋 컨벤션 정의 (Conventional Commits 추천)
☐ CI/CD 파이프라인 설계
☐ 원격 캐시 설정 계획
☐ 패키지 소유권 정의 (CODEOWNERS)
모노레포는 은탄환(Silver Bullet)이 아니야.
작은 팀이나 단순한 프로젝트에는 오히려 오버엔지니어링이 될 수 있어.
하지만 여러 앱이 코드를 공유하고, 팀이 함께 개발하는 환경이라면 모노레포는 진짜 게임 체인저가 될 수 있어!
재능넷에서 개발 관련 재능을 거래하거나 협업 프로젝트를 진행할 때도 이런 모노레포 구조를 미리 잡아두면 나중에 훨씬 수월하게 작업할 수 있을 거야. 처음 세팅에 시간을 좀 투자하면 나중에 몇 배로 돌아오거든 😊
모노레포 설정은 프로젝트가 성장하면서 함께 진화해야 해. 처음엔 간단하게 시작하고, 문제가 생길 때마다 개선해나가는 게 훨씬 현실적이야. 완벽한 설정을 처음부터 만들려다가 정작 개발은 못 하는 상황이 생기면 안 되잖아 ㅋㅋ
관련 키워드
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.