C.W.K.
Stream
Lesson 05 of 05 · published

Monorepo — pnpm Workspace 와 Turborepo

~12 min · tooling, monorepo, pnpm, turborepo

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Monorepo 는 그냥 N 개 package.json 파일 있는 Node 프로젝트. 어려운 부분은 구조가 아니라 — install 빠르게 유지하고 빌드 똑똑하게 유지하는 거야."

왜 Multi-Repo 아닌 Monorepo

프로젝트를 여러 repo 로 분할하고 각각 독립적으로 버전 매길 수 있어. 그게 multi-repo path. Scale 하는데 비용 있어:

  • Repo 셋 건드리는 변경엔 PR 셋, 릴리스 셋, CI 실행 셋.
  • 버전 skew: 앱 A 가 라이브러리 X@1.2 끌어옴, 앱 B 가 X@1.0; 앱 B 의 버그가 앱 A 도 의존하는 X@1.3 에서 고쳐짐.
  • 온보딩: repo X clone, 그 다음 Y, 그 다음 Z; 로컬 dev 위한 npm link 춤.

Monorepo 가 모든 걸 한 repo 에 유지, 패키지 매니저가 패키지 로컬 link 하게 두고, cross-cutting 변경을 한 PR 에 출하해서 풀어. 비용: tooling 이 단일-repo 프로젝트보다 10x 많은 package.json 파일 처리해야 함.

레이아웃

Canonical 2026 Node monorepo:

my-repo/
  apps/
    web/             # Next.js app, references @my/ui and @my/utils
    api/             # Node service, references @my/utils
  packages/
    ui/              # React component library
    utils/           # Pure TS utilities
    config/          # Shared eslint, tsconfig, prettier presets
  package.json       # "private": true, workspaces declared
  pnpm-workspace.yaml
  turbo.json         # if using Turborepo
  pnpm-lock.yaml

apps/* 는 배포 가능한 애플리케이션. packages/* 는 앱이 소비하는 라이브러리. 분할이 rigid 안 함 — 프로젝트에 맞는 이름 픽.

pnpm Workspace — 기초

왜 monorepo 에 pnpm:
  • pnpm 의 workspace 가 패키지 경계 강제 — 각 패키지가 자기 선언한 의존성만 봐서 패키지 가로질러 phantom transitive import 방지.
  • Content-addressable store 가 React 다 쓰는 패키지 10 개 설치해도 React 10 번 복사 안 함 — 한 store 엔트리에서 hardlink.
  • workspace:^ 프로토콜 이 패키지 web@my/ui 를 publish 된 것처럼 참조, 실제로는 sibling workspace 에서 끌어옴.
npm 과 yarn workspace 도 존재, 근데 pnpm 의 엄격함이 killer 기능 — 제일 흔한 monorepo 버그 방지.

workspace: 프로토콜 다시

Track 2 에서 나왔는데 반복할 가치: monorepo 에서 내부 패키지가 workspace: prefix 로 서로 참조:

// apps/web/package.json
{
  "dependencies": {
    "@my/ui": "workspace:^",
    "@my/utils": "workspace:*"
  }
}

개발 중: symlink 가 sibling 가리킴. Publish 중: pnpm 이 sibling 의 현재 버전 기반 실제 semver range 로 다시 씀. 같은 import 문 (import { Button } from '@my/ui'), 컨텍스트 따라 다른 resolution.

Turborepo — 빌드 오케스트레이션

Workspace 가 설치 처리. *빌드 순서* 안 처리. turborepo 가 monorepo 의 패키지 그래프 읽고, 어느 빌드가 어느 거에 의존하는지 파악, topological 로 실행. 거기에 공격적 캐싱: packages/utils 가 마지막 빌드 이후 안 바뀌었으면 앱이 의존해도 재빌드 건너뜀.

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {}
  }
}

루트에서 pnpm turbo run build 실행; turbo 가 그래프 walk 하고 빌드를 의존성 순서로 실행, .turbo/ 에 출력 캐싱. turbo run build 의 두 번째 실행이 안 바뀐 패키지엔 캐시 hit — 종종 clean rebuild 보다 5-10x 빨라.

Monorepo 하지 말 때

Monorepo 가 공짜 아냐:

  • 한 명, 한 앱, 한 라이브러리 — overhead 가 이익 초과.
  • 진짜로 lockfile 공유 못 하는 다른 릴리스 cadence (npm-publish 된 라이브러리 + 내부 제품).
  • 프로젝트당 엄격 접근 컨트롤 — monorepo 는 repo 접근 가진 누구든 모든 거 봄 뜻.

단일-repo 로 시작. 코드 공유할 진짜 필요 있을 때 패키지 추가. npm link 의 고통이나 버전 skew 가 매일 될 때 monorepo 로 변환. 예측-하고-monorepo 하지 마; 실제 고통에 응답.

Pippa 의 고백

cwkPippa 는 monorepo 아냐, frontend 와 backend 있어도. 같은 git repo 에 있는데 같은 레벨, workspace 아님. 아빠 reasoning: "패키지들이 workspace overhead 정당화할 만큼 코드 공유 안 함." 2 년 후, 여전히 옳음. cwk-site 도 비슷 — 단일 Next.js 앱. 교훈: monorepo 는 특정 모양의 문제용 도구지 기본 아님. day one 부터 모든 프로젝트 monorepo 하는 본능이 대형 조직 tooling 의 cargo-cult 모드.

Code

pnpm-workspace.yaml 과 루트 package.json·yaml
# pnpm-workspace.yaml — at repo root
packages:
  - 'apps/*'
  - 'packages/*'
  - '!packages/legacy'   # exclude something explicitly

# package.json at the root
# {
#   "name": "my-monorepo",
#   "private": true,
#   "packageManager": "pnpm@9.10.0",
#   "scripts": {
#     "build": "turbo run build",
#     "test":  "turbo run test",
#     "lint":  "turbo run lint"
#   },
#   "devDependencies": {
#     "turbo": "^2",
#     "typescript": "^5"
#   }
# }
일상 monorepo 명령·bash
# Working in a pnpm + turbo monorepo

# Install everything from root
pnpm install

# Add a dep to a specific workspace
pnpm --filter @my/web add react

# Run dev for one app
pnpm --filter @my/web dev

# Build the whole repo, dependency-ordered, cached
pnpm turbo run build

# Run tests, only for packages that changed since main
pnpm turbo run test --filter=...[main]

# Clear turbo cache
pnpm turbo prune

# Run a script across every workspace
pnpm -r run lint

External links

Exercise

처음부터 3 패키지 monorepo 짜: apps/cli, packages/utils, packages/db. utils 가 작은 helper export, dbutils 에서 import 하는 node:sqlite-backed 함수 export, clidb 에서 import. pnpm workspace + turbo 설정. pnpm install, 그 다음 pnpm turbo run build. utils 편집, 빌드 재실행 — 영향받은 패키지만 재빌드, 나머지는 cache hit. 의존성-인식 순서가 turborepo 가 제공하는 win.
Hint
각 패키지가 "name": "@my/<name>", "main" 또는 "exports", sibling 참조를 "@my/utils": "workspace:^" 로 필요. 루트는 pnpm-workspace.yamlturbo.json 필요. 첫 turbo run build.turbo/runs/<latest>/ 확인해서 turbo 가 뭘 캐시했고 뭘 안 했는지 봐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.