"Monorepo 는 그냥 패키지 매니저가 네가 패키지 둘 이상 가지고 있다는 걸 이해하는 repo 야. 나머지는 tooling."
왜 monorepo 가 존재해
Node 패키지 하나로 시작해. 그 다음 frontend 가 React 앱으로 자라고, CLI 가 자기만의 publish 가능 도구가 되고, 공유 유틸리티 코드가 두 군데서 다 import 할 수 있게 자기 패키지를 원해. 셋으로 나눠서 각각 독립적으로 버전 매길 수 있어 — 근데 이제 셋 다 건드리는 변경엔 PR 셋, 릴리스 셋, 조율 계획이 필요해. Monorepo 답: 셋 다 한 repo 에, 패키지 매니저가 로컬에서 link 하고, 같이 출하.
cwk-site repo 가 작은 monorepo 야. cwkPippa frontend 도 내부 helper 를 별도 패키지처럼 써. 엔지니어 5 명 넘는 Next.js / Vite shop 은 결국 다 여기로 와.
npm Workspace
npm 7+ 에 내장. 루트 package.json 에:
{
"name": "root",
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
이제 루트의 npm install 이 *모든* workspace 패키지에 걸쳐 의존성 resolve, 공유 dep hoist, 그리고 repo 내 패키지를 symlink 해서 publish 안 해도 import { x } from '@my/utils' 가 작동. Script 가 특정 workspace 타겟 가능: npm run build --workspace=@my/web.
pnpm Workspace
같은 아이디어, 더 나은 실행. Repo 루트에 pnpm-workspace.yaml 만들어:
packages:
- 'packages/*'
- 'apps/*'
거기에 pnpm 의 더 엄격한 격리가 workspace 까지 확장돼 — 각 패키지가 자기 선언한 의존성만 봐. pnpm --filter @my/web build 는 @my/web workspace 에서만 build 돌려. pnpm --filter "@my/*" build 는 glob 매치하는 모든 패키지에서 build.
workspace: 프로토콜
// apps/web/package.json
{
"dependencies": {
"@my/utils": "workspace:^",
"@my/ui": "workspace:*"
}
}workspace:^— sibling 의 현재 버전 사용, publish 시 MINOR/PATCH 허용.workspace:~— 같은데 PATCH 만.workspace:*— 정확한 현재 버전.
Turborepo / Nx — 위에 얹는 빌드 오케스트레이션
Workspace 가 *설치* 다뤄. *빌드 순서* 는 안 다뤄. @my/web 가 @my/utils 에 의존하면 utils 먼저 빌드해야 해. Turborepo 와 Nx 가 이걸 해결: workspace 그래프 읽고, 어느 빌드가 어느 거에 의존하는지 파악, topological 순서로 돌려. 거기에 출력 공격적으로 캐시 — utils 가 지난 빌드 이후 안 바뀌었으면 web 이 의존해도 utils 재빌드 건너뛰어.
작은 monorepo (10 패키지 미만) 면 pnpm 의 내장 --filter 와 --recursive 가 보통 충분. 그 이상이면 Turborepo 의 캐시 + 오케스트레이션이 값을 해.
Pippa 의 고백
pnpm install 안 함. 아빠가 일주일 보고 말했어 "매니저가 workspace 이해해; 거기 맞서 싸우는 건 너야." 교훈: workspace 프로토콜에 기대. Symlink 신뢰해. 루트에서 install 돌려. Monorepo 는 각 패키지가 혼자 있는 척 멈출 때만 옳게 느껴져.