TL;DR
- Yarn Workspaces와 Nx는 같은 층의 도구가 아니다.
- Yarn은 의존성 설치와 workspace linking을 맡고, Nx는 작업 그래프와 캐시를 맡는다.
- Yarn Classic과 Yarn Berry는 세대 선택 관계지만, Yarn과 Nx는 같이 쓸 수 있다.
- 낯선 모노레포는 도구 이름보다 lockfile,
package.json,nx.json,project.json을 먼저 읽는 편이 빠르다.
운영 중인 웹뷰 서비스 저장소는 모노레포였다. 앱 몇 개와 공통 패키지가 한 저장소에 있었다. 코드를 고치기 전에 알아야 할 것은 변경 영향 범위였다. 처음 보기에는 질문이 이렇게 보인다.
이 저장소는 Nx인가?
Yarn Workspaces인가?
Yarn Berry인가?
하지만 이 질문은 층위가 섞여 있다. 셋은 모두 모노레포 문맥에서 등장하지만 같은 일을 하지 않는다.
모노레포의 관심사
모노레포에서 해결해야 하는 문제는 여러 개다.
| 관심사 | 질문 |
|---|---|
| 의존성 설치 | 각 package의 dependencies를 어떻게 설치하는가 |
| 내부 패키지 연결 | @repo/ui 같은 패키지를 로컬로 연결하는가 |
| 명령 실행 | 어떤 프로젝트에서 build, test, lint를 실행하는가 |
| 작업 순서 | 공통 패키지가 바뀌면 어떤 앱을 다시 빌드하는가 |
| 캐시 | 이전 실행 결과를 재사용하는가 |
| 영향 범위 | 이번 변경이 어디까지 퍼지는가 |
패키지 매니저는 주로 설치와 연결을 맡는다. Nx는 작업 그래프, affected 계산, cache를 맡는다.
Package Manager
-> install
-> lockfile
-> workspace link
Nx
-> project graph
-> task graph
-> affected
-> cache
따라서 “Nx냐 Yarn이냐”는 선택지가 아니다. Yarn으로 설치하고 Nx로 작업을 실행할 수 있다.
Yarn Classic과 Berry
Yarn Classic은 Yarn 1.x다. workspace 기능은 루트 package.json의 workspaces 필드로 정의한다.
{
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
Classic에서는 내부 패키지도 일반 semver 범위처럼 적는다.
{
"dependencies": {
"@repo/api-client": "1.0.0"
}
}
로컬 workspace의 package name과 version이 이 범위에 맞으면 로컬로 연결된다. 맞지 않으면 registry에서 설치될 수 있다. Yarn Berry는 Yarn 2+ 세대다. workspace: protocol을 쓸 수 있고, .yarnrc.yml로 설치 방식을 제어한다.
{
"dependencies": {
"@repo/api-client": "workspace:*"
}
}
nodeLinker: node-modules
Berry의 기본 모델은 Plug'n'Play지만, nodeLinker를 통해 node_modules 방식도 선택할 수 있다. Classic과 Berry는 같은 패키지 매니저의 세대 차이다. 저장소는 둘 중 하나를 선택한다.
Nx의 위치
Nx는 패키지 매니저가 아니다. 저장소 안의 프로젝트와 target을 읽고 작업 그래프를 만든다.
{
"targets": {
"build": {
"executor": "@nx/vite:build"
},
"test": {
"executor": "@nx/jest:jest"
}
}
}
nx build web은 단순히 web 폴더에서 shell script를 실행하는 것이 아니다. Nx는 project graph와 target dependency를 보고 필요한 작업 순서와 cache hit 여부를 계산한다. 반면 yarn workspace web build는 해당 workspace의 script를 실행한다. Nx의 affected 계산이나 remote cache 정책은 적용되지 않는다.
| 명령 | 주체 | 특징 |
|---|---|---|
yarn install |
Yarn | 의존성 설치 |
yarn workspace web build |
Yarn script | 해당 workspace script 실행 |
nx build web |
Nx | project graph, cache, target 설정 사용 |
nx affected -t build |
Nx | 변경 영향 범위 기반 실행 |
파일 읽는 순서
낯선 모노레포에서는 설명보다 파일이 빠르다.
1. lockfile
yarn.lock
package-lock.json
pnpm-lock.yaml
어떤 패키지 매니저로 설치해야 하는지 먼저 확인한다. lockfile이 답이다.
2. 루트 package.json
{
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
workspace 범위를 확인한다. 이 배열 밖의 폴더는 같은 저장소에 있어도 workspace가 아닐 수 있다.
3. dependency 표기
"@repo/ui": "workspace:*"
workspace: protocol이 있으면 Yarn Classic이 아니다. Classic 저장소라면 일반 semver 범위를 더 자주 본다.
4. .yarnrc.yml
nodeLinker: pnp
Yarn Berry 저장소라면 nodeLinker를 확인한다. pnp인지 node-modules인지에 따라 도구 호환성과 로컬 디버깅 방식이 달라진다.
5. nx.json / project.json
루트 nx.json이 있으면 Nx가 작업 그래프를 관리한다. 각 프로젝트의 project.json 또는 package.json 안 target을 확인한다.
6. tsconfig paths
{
"paths": {
"@repo/ui": ["packages/ui/src/index.ts"]
}
}
내부 패키지를 source로 직접 참조하는지, build output을 참조하는지 확인한다. 이 차이가 변경 후 검증 범위를 바꾼다.
비교표
셋을 한 줄로 비교하면 다음과 같다.
| 항목 | Yarn Classic Workspaces | Yarn Berry | Nx |
|---|---|---|---|
| 정체 | Yarn 1.x workspace 기능 | Yarn 2+ 패키지 매니저 | 작업 그래프 도구 |
| 주 역할 | 설치, 내부 패키지 연결 | 설치, workspace protocol, PnP 등 | target 실행, affected, cache |
| 대체 관계 | Berry와 대체 | Classic과 대체 | Yarn과 대체 아님 |
| 같이 쓰기 | Nx와 가능 | Nx와 가능 | npm/Yarn/pnpm과 가능 |
핵심은 대체 관계다. Yarn Classic과 Berry는 같은 층이다. Yarn과 Nx는 층이 다르다.
정리
모노레포를 읽을 때 도구 이름 하나로 저장소를 설명하려고 하면 놓치는 것이 생긴다. 정확한 질문은 이쪽이다.
누가 설치하는가?
누가 내부 패키지를 연결하는가?
누가 작업 순서를 계산하는가?
누가 cache를 관리하는가?
Yarn Workspaces는 설치와 연결의 답이고, Nx는 작업 실행과 영향 범위의 답이다. 둘은 경쟁하지 않기 때문에 한 저장소에 같이 있을 수 있다. 낯선 모노레포에서는 먼저 파일을 읽는다. lockfile이 설치 주체를 말하고, workspaces가 패키지 범위를 말하고, nx.json과 project.json이 작업 그래프를 말한다. 도구 이름은 그 다음에 붙여도 늦지 않다.