테마
Yarn 워크스페이스 (Classic과 Berry)
Yarn Classic(1.x)과 Yarn Berry(2.x+)의 워크스페이스 기능을 비교하고, PnP(Plug'n'Play) 모드의 혁신적인 의존성 해석 방식을 이해한다.
학습 목표
- Yarn Classic(1.x)의 워크스페이스 설정 방법과 특징을 이해한다.
- Yarn Berry(2.x+)의 워크스페이스 설정 방법과 PnP 모드의 동작 원리를 파악한다.
- PnP(Plug'n'Play)가 기존
node_modules방식의 어떤 문제를 해결하는지 이해한다. - Classic과 Berry 두 버전의 장단점을 비교하여 적절한 선택 기준을 세운다.
1. Yarn Classic (1.x) 워크스페이스
1.1 등장 배경
Yarn은 2016년 Facebook, Google 등이 npm의 아쉬운 점을 개선하기 위해 만들었다. Yarn Classic의 워크스페이스 기능은 npm보다 먼저 도입되어, 모노레포 구성의 사실상 표준이 되었다.
npm 대비 개선점:
yarn.lock파일로 일관된 의존성 재설치 보장- 병렬 다운로드로 설치 속도 향상
- 오프라인 캐시 지원
- 워크스페이스 기능 최초 도입
1.2 설정 방법
1) 루트 프로젝트 초기화
bash
mkdir yarn-classic-example && cd yarn-classic-example
yarn init -y -p # -p: private을 true로 설정Yarn Classic 워크스페이스를 사용하려면 루트 package.json에 **"private": true**가 반드시 필요하다. 워크스페이스 루트가 npm 레지스트리에 배포되는 것을 방지하기 위함이다.
2) 워크스페이스 선언
json
// 루트 package.json
{
"name": "yarn-classic-example",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
]
}3) 패키지 생성
bash
mkdir -p packages/a packages/b
cd packages/a && yarn init -y
cd ../b && yarn init -y4) 의존성 설치
bash
# 루트에서 전체 설치
yarn
# 특정 워크스페이스에 의존성 추가
yarn workspace a add axios
# 특정 워크스페이스의 스크립트 실행
yarn workspace a start
# 모든 워크스페이스에서 스크립트 실행
yarn workspaces run start1.3 node_modules 구조 (호이스팅)
Yarn Classic은 npm과 동일한 평탄한(flat) node_modules 구조를 사용한다.
yarn-classic-example/
node_modules/
a -> ../packages/a # 심볼릭 링크
b -> ../packages/b # 심볼릭 링크
axios/ # 호이스팅된 패키지
follow-redirects/ # axios의 의존성 (호이스팅)
packages/
a/
package.json
index.js
b/
package.json
index.js1.4 Yarn Classic의 한계
npm과 동일한 평탄 구조를 사용하므로 동일한 한계를 가진다.
- Phantom Dependency: 선언하지 않은 의존성에 접근 가능
- 디스크 비효율: 프로젝트마다 패키지가 복사됨
- 호이스팅 비결정성: 설치 순서에 따라 호이스팅 결과가 달라질 수 있음
2. Yarn Berry (2.x+) 워크스페이스
2.1 등장 배경
2020년에 출시된 Yarn Berry는 Yarn Classic과 완전히 다른 아키텍처를 가진다. 가장 혁신적인 점은 node_modules 폴더를 아예 사용하지 않는 Plug'n'Play (PnP) 모드이다.
2.2 설정 방법
1) Yarn Berry 활성화
bash
mkdir yarn-berry-example && cd yarn-berry-example
npm init -y
# corepack을 통한 Yarn Berry 활성화
corepack enable
corepack use yarn@4.0.0
# 또는 yarn set version 명령 사용
yarn set version berrycorepack use 명령을 실행하면 package.json에 packageManager 필드가 추가된다.
json
{
"name": "yarn-berry-example",
"packageManager": "yarn@4.0.0"
}2) 워크스페이스 선언
json
// 루트 package.json
{
"name": "yarn-berry-example",
"packageManager": "yarn@4.0.0",
"workspaces": [
"packages/*"
]
}Berry에서는 "private": true가 필수가 아니다 (선택 사항).
3) 패키지 생성 및 설치
bash
mkdir -p packages/a packages/b
cd packages/a && yarn init
cd ../b && yarn init
cd ../..
yarn install4) 워크스페이스 명령어
bash
# 특정 워크스페이스에 의존성 추가
yarn workspace a add axios
# 특정 워크스페이스의 스크립트 실행
yarn workspace a start
# 모든 워크스페이스에서 실행
yarn workspaces foreach run build
# 의존 순서를 고려하여 실행 (토폴로지 정렬)
yarn workspaces foreach --topological run build3. Plug'n'Play (PnP) 모드
3.1 PnP의 핵심 개념
PnP 모드에서는 node_modules 폴더가 생성되지 않는다. 대신:
- 모든 패키지가
.yarn/cache/디렉터리에 zip 파일로 저장된다. .pnp.cjs파일이 의존성 해석 테이블 역할을 한다.- Node.js가 모듈을 탐색할 때
.pnp.cjs가 가로채서 올바른 zip 파일 내부 경로를 반환한다.
yarn-berry-example/
.pnp.cjs # 의존성 해석 맵
.pnp.loader.mjs # ESM 로더
.yarn/
cache/ # 패키지 zip 파일들
axios-npm-1.6.0-abc123.zip
lodash-npm-4.17.21-def456.zip
releases/
yarn-4.0.0.cjs # Yarn Berry 바이너리
.yarnrc.yml # Yarn 설정 파일
package.json
packages/
a/
package.json
index.js
b/
package.json
index.js3.2 .pnp.cjs 파일의 역할
.pnp.cjs는 다음과 같은 정보를 담고 있다.
javascript
// .pnp.cjs (간략화된 구조)
["axios", [
["npm:1.6.0", {
packageLocation: "./.yarn/cache/axios-npm-1.6.0-abc123.zip/node_modules/axios/",
packageDependencies: [
["follow-redirects", "npm:1.15.0"],
["form-data", "npm:4.0.0"],
],
}],
]],- 패키지 이름 -> 위치 매핑: 각 패키지가 어느 zip 파일에 있는지 기록
- 의존성 선언 검증:
a가axios를 사용하려면a의package.json에 선언되어 있어야 함 - 즉시 해석: 파일 시스템 탐색 없이 테이블 조회로 바로 위치 반환
3.3 엄격한 의존성 관리
PnP 모드에서는 package.json에 선언하지 않은 패키지를 사용하려 하면 즉시 에러가 발생한다.
javascript
// packages/a/package.json에 axios가 없는 경우
const axios = require('axios');
// Error: a tried to access axios, but it isn't declared in its dependencies이로써 Phantom Dependency 문제가 원천 차단된다.
3.4 Zero-Install
.pnp.cjs와 .yarn/cache/ 디렉터리를 git에 커밋하면, 다른 개발자가 저장소를 클론한 후 yarn install 없이 바로 프로젝트를 실행할 수 있다. 이를 Zero-Install이라 한다.
yaml
# .yarnrc.yml
enableGlobalCache: false # 프로젝트 로컬 캐시 사용
nodeLinker: pnp # PnP 모드 활성화 (기본값)3.5 node_modules 호환 모드
PnP와 호환되지 않는 도구가 있을 경우, Berry에서도 node_modules를 사용하도록 전환할 수 있다.
yaml
# .yarnrc.yml
nodeLinker: node-modules # node_modules 모드로 전환이 경우 Yarn Classic과 유사한 평탄 구조가 생성되지만, Berry의 다른 기능(프로토콜, 플러그인 등)은 그대로 사용할 수 있다.
4. Yarn Classic vs Yarn Berry 비교
| 비교 항목 | Yarn Classic (1.x) | Yarn Berry (2.x+) |
|---|---|---|
| 의존성 저장 | node_modules 폴더 | .yarn/cache/ (zip) |
| 의존성 해석 | Node.js 기본 탐색 | .pnp.cjs 테이블 조회 |
| node_modules 구조 | 평탄(flat) | 없음 (PnP) 또는 선택적 |
| Phantom Dependency | 발생 | 방지 |
| Zero-Install | 불가 | 가능 |
| 설치 속도 | 빠름 (npm 대비) | 매우 빠름 |
| 디스크 사용량 | 높음 | 낮음 (zip 압축) |
| 생태계 호환성 | 매우 높음 | PnP 비호환 도구 존재 |
| 루트 private 필수 | 필수 | 선택 |
| 플러그인 시스템 | 없음 | 있음 |
| 워크스페이스 명령 | yarn workspace <name> | yarn workspace <name> |
| 전체 실행 명령 | yarn workspaces run | yarn workspaces foreach |
5. Yarn Berry의 장단점
5.1 장점
- Phantom Dependency 해결: 엄격한 의존성 검증으로 선언하지 않은 패키지 사용 차단
- 빠른 설치: zip 파일을 커밋하면 설치 과정 자체가 불필요 (Zero-Install)
- 디스크 절약: zip 압축으로 저장하며, 같은 패키지를 중복 저장하지 않음
- 결정적(deterministic) 설치:
.pnp.cjs가 정확한 의존성 트리를 보장 - 플러그인 확장: 커스텀 플러그인으로 기능 확장 가능
5.2 단점
- 호환성 문제: PnP를 지원하지 않는 도구가 있음 (점차 개선 중)
- 일부 도구는
node_modules경로를 하드코딩 @yarnpkg/pnpify패키지로 호환성 레이어 제공
- 일부 도구는
- 학습 비용:
.pnp.cjs,.yarnrc.yml, 프로토콜 등 새로운 개념이 많음 - git 저장소 크기: Zero-Install 사용 시
.yarn/cache가 커밋되어 저장소가 커짐 - 디버깅 복잡도: zip 내부의 소스를 직접 수정하기 어려움
5.3 PnP 호환성 해결 전략
bash
# 호환되지 않는 패키지가 있을 경우
# 1. packageExtensions로 누락된 의존성 추가
# .yarnrc.yml
packageExtensions:
"some-package@*":
dependencies:
"missing-dep": "*"
# 2. node_modules 모드로 전환
# .yarnrc.yml
nodeLinker: node-modules
# 3. pnpify로 호환성 레이어 추가
yarn add @yarnpkg/pnpify
yarn pnpify some-tool6. 실습: 워크스페이스 내부 패키지 의존 관계
Yarn Berry에서 워크스페이스 a가 워크스페이스 b를 사용하려면 반드시 명시적으로 선언해야 한다.
json
// packages/a/package.json
{
"name": "a",
"version": "1.0.0",
"dependencies": {
"b": "workspace:*"
}
}workspace:* 프로토콜은 Yarn Berry 전용 기능으로, 로컬 워크스페이스를 명시적으로 참조한다. 패키지를 npm에 배포할 때 자동으로 실제 버전으로 변환된다.
javascript
// packages/b/index.js
const axios = require('axios');
module.exports = async function fetchUsers() {
const response = await axios.get('https://api.github.com/users');
return response.data;
};javascript
// packages/a/index.js
const fetchUsers = require('b');
(async function main() {
const users = await fetchUsers();
console.log(users.map(u => u.login).join(', '));
})();PnP 모드에서 b의 package.json에 axios가 없으면 즉시 에러가 발생하므로, 올바른 위치에 의존성을 선언하도록 강제된다.
핵심 정리
- **Yarn Classic(1.x)**은 npm보다 빠른 설치, lock 파일, 워크스페이스를 최초 도입했지만, 평탄한
node_modules구조의 한계(Phantom Dependency)는 그대로 가지고 있다. - **Yarn Berry(2.x+)**는
node_modules를 버리고 PnP(Plug'n'Play) 모드를 도입하여,.pnp.cjs테이블과 zip 캐시로 의존성을 관리한다. - PnP 모드는 Phantom Dependency를 원천 차단하고, Zero-Install로 설치 과정 자체를 생략할 수 있다.
- PnP와 호환되지 않는 도구가 있을 수 있으나,
nodeLinker: node-modules설정으로 기존 방식으로 전환할 수 있다. - Berry에서는
workspace:*프로토콜로 워크스페이스 간 의존 관계를 명시적으로 선언한다.
다음 단계
- 04. pnpm 워크스페이스: pnpm의 하드링크/심볼릭 링크 전략, 비-평탄
node_modules구조, 그리고 마이크로프론트엔드에서 pnpm이 선호되는 이유를 알아본다.