Yarn 6 Deep Dive: Rust로 다시 쓴 패키지 매니저
TypeScript Berry와 Rust Yarn 6의 실제 코드를 비교하며 성능 개선의 원리와 아키텍처 변화를 분석합니다.
2026년 1월, Yarn의 리드 메인테이너 Maël Nison은 Yarn을 Rust로 포팅하겠다는 계획을 공식 발표했습니다. 약 1년의 개발 끝에 프리뷰 단계에 진입한 Yarn 6(코드네임 zpm)는, JavaScript 생태계에서 오랫동안 사용된 패키지 매니저의 구현 방식을 근본적으로 바꾸는 시도입니다.
처음 소식을 봤을 때는 “정말 체감될 만큼 달라질까?”가 가장 궁금했습니다. 이 글에서는 단순히 “빨라졌다”는 결론에 머무르지 않고, 실제 코드를 열어보며 왜 빨라졌는지, 무엇이 달라졌는지, 그리고 그 선택이 어떤 의미를 갖는지를 정리합니다.
1. Rust 선택 배경: Berry의 한계
Berry의 구조적 한계
Yarn Berry(v2~v4)는 패키지 매니저 역사에서 가장 야심찬 프로젝트 중 하나였습니다. Plug’n’Play, Zero Installs, 플러그인 아키텍처, constraints 등 혁신적인 개념을 도입했고, 그 설계 자체는 훌륭했습니다.
하지만 대규모 모노레포를 운영하는 팀들에서 같은 패턴의 불만이 반복되었습니다.
1. 순차적 설치 파이프라인
Berry의 Project.install()은 해석(resolve) → 페칭(fetch) → 링킹(link)을 엄격하게 순서대로 실행합니다.
패키지 A의 해석이 끝나도 B의 해석이 끝날 때까지 A의 페칭을 시작할 수 없습니다.
의존성이 수천 개인 프로젝트에서, 이 단계 간 대기 시간은 무시할 수 없는 수준이 됩니다.
2. 플러그인 시스템의 런타임 오버헤드
Berry의 핵심 강점인 플러그인 아키텍처는, 동시에 성능 병목의 원인이기도 합니다.
21개의 플러그인이 각각 Resolver, Fetcher, Linker 인터페이스를 구현하고, 매 패키지마다 “이 플러그인이 지원하는가?”를 런타임에 순회하며 확인합니다.
V8 엔진은 호출 대상이 너무 다양한 다형성 호출(megamorphic call site)을 최적화하지 못합니다.
3. lockfile 파싱의 반복 비용
수만 줄에 달하는 lockfile을 매 설치마다 YAML로 파싱하고, JavaScript 객체로 변환하고, 가비지 컬렉션의 대상이 되는 수만 개의 객체를 힙에 할당합니다. 이 과정은 warm cache에서도 피할 수 없는 고정 비용입니다.
4. 싱글 스레드의 벽
Node.js는 I/O 비동기를 지원하지만, CPU 바운드 작업은 결국 싱글 스레드에서 처리됩니다. 의존성 그래프 계산, 체크섬 검증, lockfile 직렬화 같은 CPU 집약적 작업을 병렬화할 수 없습니다.
이 문제들은 Berry의 코드 품질과 무관합니다. TypeScript와 Node.js라는 플랫폼 자체의 구조적 한계에서 비롯된 것입니다. 그리고 이것이 Yarn 팀이 “더 나은 TypeScript 코드”가 아닌 “다른 언어”를 선택한 이유입니다.
JavaScript 생태계의 네이티브 전환
Yarn만 이런 판단을 내린 것은 아닙니다. JavaScript 생태계 전반에서 성능 크리티컬한 툴링을 네이티브 언어로 재작성하는 흐름이 이어지고 있습니다.
| 도구 | 기존 (JS/TS) | 신규 (Rust/Zig) |
|---|---|---|
| 번들러 | webpack | Rspack (Rust), Turbopack (Rust), Rolldown (Rust) |
| 린터/포매터 | ESLint + Prettier | Biome (Rust), oxlint (Rust) |
| 트랜스파일러 | Babel, tsc | SWC (Rust), oxc (Rust) |
| 패키지 매니저 | Yarn Berry (TS), npm (JS) | Yarn 6 (Rust), Bun (Zig) |
이 흐름의 핵심은 단순한 “언어 교체”가 아닙니다. JavaScript 런타임의 본질적 한계, 즉 싱글 스레드 실행 모델, 가비지 컬렉션 오버헤드, 직렬화/역직렬화 비용이 대규모 모노레포에서 체감 가능한 병목으로 작용하기 시작했다는 것입니다.
Yarn 6의 Cargo.toml을 살펴보면, 이 선택이 얼마나 의도적인지 드러납니다.
# Yarn 6 (zpm) 핵심 의존성
tokio = { version = "1.39.2", features = ["full"] } # 멀티스레드 async 런타임
rayon = "1.10.0" # 데이터 병렬 처리
rkyv = "0.8" # zero-copy 역직렬화
dashmap = "6" # 락프리 동시 해시맵
tokio로 비동기 I/O를, rayon으로 CPU 바운드 병렬 처리를, rkyv로 직렬화 오버헤드를 줄이고, dashmap으로 락 경합이 낮은 동시 자료구조를 사용합니다.
이것은 Node.js에서는 구조적으로 불가능한 조합입니다.
2. 성능 벤치마크
공식 벤치마크 결과는 다음과 같습니다.
Warm Cache 벤치마크
| 프로젝트 | Berry (v4) | Yarn 6 | 개선율 |
|---|---|---|---|
| Next.js | 577ms | 184ms | 68% 단축 |
| Gatsby | 1.7s | 0.3s | 82% 단축 |
Warm cache 벤치마크에 주목해야 하는 이유가 있습니다. 네트워크 변수를 제거한 상태이므로, 순수하게 런타임과 자료구조 수준의 최적화가 얼마나 효과적인지를 보여주는 지표이기 때문입니다.
그렇다면 구체적으로 어디서 이 차이가 발생하는 걸까요? 이를 이해하려면 코드를 직접 들여다봐야 합니다.
3. 아키텍처 비교: Berry vs Yarn 6
이 섹션은 두 레포의 실제 소스 코드를 비교해, 아키텍처 수준의 차이를 확인하는 파트입니다.
- Yarn Berry: https://github.com/yarnpkg/berry
- Yarn 6 (zpm): https://github.com/yarnpkg/zpm
전체 구조: 47개 패키지 → 14개 crate
Yarn 6의 main.rs를 먼저 보겠습니다.
// packages/zpm/src/main.rs
extern crate zpm_allocator;
use std::process::ExitCode;
#[tokio::main]
async fn main() -> ExitCode {
env_logger::init();
zpm::commands::run_default(None).await
}
코드는 단 8줄이지만, 설계 방향이 명확하게 드러납니다.
zpm_allocator로 커스텀 메모리 할당자를 주입하고, #[tokio::main]으로 멀티스레드 비동기 런타임 위에서 실행하며, 모든 로직은 zpm::commands 모듈로 위임합니다.
이 단순한 진입점이 어떤 구조 위에서 작동하는지 살펴보겠습니다.
Berry는 47개의 npm 패키지로 구성된 모노레포입니다.
packages/
├── yarnpkg-core/ # 핵심 엔진
├── yarnpkg-cli/ # CLI 진입점
├── yarnpkg-fslib/ # 파일시스템 추상화
├── yarnpkg-parsers/ # 파서
├── yarnpkg-shell/ # 내장 셸
├── yarnpkg-pnp/ # PnP 런타임
├── yarnpkg-nm/ # node_modules 생성
├── plugin-essentials/ # 필수 명령어
├── plugin-npm/ # npm 레지스트리
├── plugin-git/ # Git 의존성
├── plugin-pnp/ # PnP 링커
├── plugin-nm/ # node_modules 링커
├── ... (21개 플러그인)
└── ... (기타 유틸리티)
Yarn 6은 14개의 Rust crate로 재편됩니다.
packages/
├── zpm/ # 메인 바이너리 (commands, resolvers, fetchers, linker 포함)
├── zpm-config/ # 설정 관리
├── zpm-primitives/ # 기본 타입 (Ident, Descriptor, Locator 등)
├── zpm-semver/ # SemVer 파서/비교기
├── zpm-parsers/ # lockfile 등 파서
├── zpm-formats/ # 형식 변환
├── zpm-git/ # Git 유틸리티
├── zpm-sync/ # 동기화
├── zpm-utils/ # 공통 유틸리티
├── zpm-switch/ # Yarn Switch 바이너리
├── zpm-constraints/ # 제약 조건 엔진
├── zpm-allocator/ # 커스텀 메모리 할당자
├── zpm-macro-enum/ # 매크로 유틸리티
└── zpm-macro-helpers/ # 매크로 헬퍼
가장 눈에 띄는 변화는 플러그인 시스템의 축소/통합입니다.
Berry에서는 Resolver, Fetcher, Linker가 각각 독립된 플러그인 패키지로 분리되어 있었습니다.
plugin-npm은 npm resolver와 fetcher를, plugin-git은 git resolver와 fetcher를 각각 제공하는 식이었죠.
Yarn 6에서는 이 역할이 zpm crate 내부의 서브모듈로 통합됩니다.
확장성보다 컴파일 타임 최적화와 낮은 런타임 오버헤드를 우선한 트레이드오프로 볼 수 있습니다.
Resolution 파이프라인: interface 다형성 → enum 디스패치
의존성 해석(Resolution)은 패키지 매니저의 심장입니다. "^4.0.0"이라는 범위를 "4.28.0"이라는 구체적 버전으로 변환하는 과정이죠.
Berry의 접근: TypeScript interface 기반 플러그인
// packages/yarnpkg-core/sources/Resolver.ts
export interface Resolver {
supportsDescriptor(
descriptor: Descriptor,
opts: MinimalResolveOptions,
): boolean;
supportsLocator(locator: Locator, opts: MinimalResolveOptions): boolean;
shouldPersistResolution(
locator: Locator,
opts: MinimalResolveOptions,
): boolean;
bindDescriptor(
descriptor: Descriptor,
locator: Locator,
opts: MinimalResolveOptions,
): Descriptor;
getResolutionDependencies(
descriptor: Descriptor,
opts: MinimalResolveOptions,
): Record<string, Descriptor>;
getCandidates(
descriptor: Descriptor,
dependencies: Record<string, Package>,
opts: ResolveOptions,
): Promise<Locator[]>;
getSatisfying(
descriptor: Descriptor,
dependencies: Record<string, Package>,
locators: Locator[],
opts: ResolveOptions,
): Promise<{ locators: Locator[]; sorted: boolean }>;
resolve(locator: Locator, opts: ResolveOptions): Promise<Package>;
}
Berry에서는 각 플러그인이 이 Resolver 인터페이스를 구현합니다. plugin-npm의 NpmSemverResolver, plugin-git의 GitResolver 등이 각각 독립된 패키지에서 이 인터페이스를 구현하고, 런타임에 등록됩니다.
이 설계는 확장성이 높지만, 런타임 다형성에 의존해 V8 인라이닝이 어려워지는 “megamorphic call site” 문제가 생길 수 있습니다.
Yarn 6의 접근: enum 기반 정적 디스패치
// packages/zpm/src/resolvers/mod.rs (구조 재현)
pub async fn resolve_descriptor(
ctx: &InstallContext<'_>,
descriptor: &Descriptor,
) -> Result<Vec<Locator>> {
match &descriptor.range {
Range::Builtin(params) => builtin::resolve_builtin_descriptor(ctx, descriptor, params).await,
Range::Git(params) => git::resolve_descriptor(ctx, descriptor, params).await,
Range::Npm(params) => npm::resolve_descriptor(ctx, descriptor, params).await,
Range::Semver(params) => semver::resolve_descriptor(ctx, descriptor, params).await,
Range::Tag(params) => tag::resolve_descriptor(ctx, descriptor, params).await,
Range::Tarball(params) => tarball::resolve_descriptor(ctx, descriptor, params).await,
Range::Url(params) => url::resolve_descriptor(ctx, descriptor, params).await,
Range::Workspace(params) => workspace::resolve_descriptor(ctx, descriptor, params).await,
Range::Folder(params) => folder::resolve_descriptor(ctx, descriptor, params).await,
Range::Link(params) => link::resolve_descriptor(ctx, descriptor, params).await,
Range::Portal(params) => portal::resolve_descriptor(ctx, descriptor, params).await,
Range::Patch(params) => patch::resolve_descriptor(ctx, descriptor, params).await,
Range::Catalog(params) => catalog::resolve_descriptor(ctx, descriptor, params).await,
}
}
Yarn 6에서는 trait 다형성 대신 enum match를 사용합니다.
Rust 컴파일러는 이 패턴 매칭을 점프 테이블로 최적화할 수 있고, 각 분기의 함수는 컴파일 타임에 완전히 인라이닝 가능합니다.
이는 단순한 코딩 스타일 차이가 아닙니다. 수만 개 패키지를 해석하는 모노레포에서는, 패키지마다 반복되는 디스패치 비용의 누적이 실제 성능 차이로 이어질 수 있습니다.
설치 파이프라인: 순차 → 그래프 기반 병렬 실행
Berry의 설치 흐름
Berry의 Project.ts에서 설치는 명확하게 순차적인 3단계로 진행됩니다.
// packages/yarnpkg-core/sources/Project.ts
async install(opts: InstallOptions): Promise<void> {
// 1단계: 모든 의존성 해석
await this.resolveEverything(opts);
// 2단계: 모든 패키지 페칭
await this.fetchEverything(opts);
// 3단계: 모든 패키지 링킹
await this.linkEverything(opts);
}
resolveEverything()이 끝나야 fetchEverything()이 시작되고, 그것이 끝나야 linkEverything()이 시작됩니다.
각 단계 내부에서는 비동기 처리가 있지만, 단계 간 파이프라이닝은 없습니다.
Yarn 6의 설치 흐름: 그래프 기반 태스크 스케줄러
// packages/zpm/src/install.rs (구조)
pub enum InstallOp {
Resolve { descriptor: Descriptor },
Fetch { locator: Locator },
Refresh { locator: Locator },
Validate { descriptor: Descriptor, locator: Locator },
}
Yarn 6에서는 해석, 페칭, 검증을 별도의 “단계”로 나누지 않습니다.
대신 각 작업을 InstallOp enum으로 표현하고, 의존성 그래프를 기반으로 스케줄링합니다.
// packages/zpm/src/graph.rs (핵심 구조)
pub struct GraphTasks<T: GraphIn> {
ready: Vec<T>, // 실행 준비된 태스크
running: FuturesUnordered<...>, // 현재 실행 중인 비동기 태스크
results: HashMap<T::Key, T::Out>, // 완료된 결과
tasks: HashMap<T::Key, TaskInfo>, // 태스크 메타데이터
dependents: HashMap<T::Key, Vec<T::Key>>, // 역방향 의존성
}
이 스케줄러는 최대 100개의 태스크를 동시에 실행하며, 한 패키지의 해석이 끝나면 즉시 페칭을 시작할 수 있습니다.
Berry: [--- resolve all ---][--- fetch all ---][--- link all ---]
Yarn 6: [resolve A][fetch A]
[resolve B][fetch B]
[resolve C][fetch C][link C]
...동시 진행...
이 차이는 의존성이 많은 대규모 프로젝트에서 더 크게 나타납니다. A 패키지 해석이 끝나는 즉시 B를 기다리지 않고 A 페칭을 시작할 수 있기 때문입니다.
Lockfile 처리: YAML 파싱 → zero-copy 역직렬화
대규모 모노레포의 lockfile은 수만 줄에 달합니다. 이 파일을 읽고 파싱하는 비용은 매 설치마다 발생하는 고정 비용입니다.
Berry: YAML 파싱
Berry는 자체 YAML 파서(yarnpkg-parsers)로 lockfile을 파싱합니다.
텍스트를 읽고 → 토큰화하고 → AST를 구성하고 → JavaScript 객체로 변환하는 전체 과정을 매번 거칩니다.
Yarn 6: rkyv를 활용한 zero-copy 역직렬화
# Cargo.toml
rkyv = "0.8" # zero-copy deserialization
rkyv는 Rust의 zero-copy 역직렬화 라이브러리입니다.
직렬화된 데이터를 파싱 없이 메모리에 매핑해 역직렬화 비용을 크게 줄입니다.
전통적인 직렬화/역직렬화:
파일 읽기 → 텍스트 파싱 → 토큰화 → AST → 객체 할당 → 사용
rkyv의 zero-copy:
파일 읽기 → 메모리 매핑 → 바로 사용 (추가 할당 없음)
이 구조는 warm cache 벤치마크에서 Gatsby가 1.7초에서 0.3초로 단축된 결과를 설명하는 중요한 요인 중 하나입니다. lockfile이 클수록 영향이 더 커질 가능성이 높습니다.
Linker: 플러그인 분리 → 단일 모듈 통합
Berry의 Linker
// packages/yarnpkg-core/sources/Linker.ts
export interface Linker {
supportsPackage(pkg: Package, opts: MinimalLinkOptions): boolean;
findPackageLocation(
locator: Locator,
opts: LinkOptions,
): Promise<PortablePath>;
findPackageLocator(
location: PortablePath,
opts: LinkOptions,
): Promise<Locator | null>;
makeInstaller(opts: LinkOptions): Installer;
}
Berry에서 PnP와 node_modules는 완전히 독립된 플러그인입니다.
plugin-pnp와 plugin-nm은 각자의 패키지에서 Linker 인터페이스를 구현합니다.
Yarn 6의 Linker
packages/zpm/src/linker/
├── mod.rs # 링커 진입점
├── helpers.rs # 공통 헬퍼
├── pnp.rs # PnP 링커
├── pnpm.rs # pnpm 호환 링커
├── nm/ # node_modules 링커 (하위 디렉토리)
├── pnp-cjs.brotli.dat # PnP 런타임 (brotli 압축)
└── pnp-mjs.brotli.dat # PnP 런타임 (ESM, brotli 압축)
주목할 점은 pnp-cjs.brotli.dat와 pnp-mjs.brotli.dat입니다.
Berry에서는 PnP 런타임 훅(.pnp.cjs)을 JavaScript로 생성했지만, Yarn 6에서는 미리 컴파일된 런타임을 brotli 압축하여 바이너리에 내장합니다.
설치 시 이 데이터를 풀어서 .pnp.cjs를 생성하므로, 런타임 생성 비용이 크게 줄어듭니다.
또한 pnpm.rs의 존재는 Yarn 6가 pnpm 스타일 링킹도 지원할 계획임을 시사합니다.
특별히 주목할 구현: clipanion-rs와 pnp-rs
Yarn 생태계에서 흥미로운 점은, Maël Nison이 자신이 만든 TypeScript 라이브러리를 직접 Rust로 포팅했다는 것입니다.
- clipanion → clipanion-rs: Yarn의 CLI 프레임워크. 타입 안전한 명령어 파싱과 서브커맨드 라우팅을 제공합니다.
- @yarnpkg/pnp → pnp-rs: PnP 해석 로직의 Rust 구현.
# Cargo.toml - Git 의존성으로 관리
clipanion = { git = "https://github.com/arcanis/clipanion-rs.git", features = ["serde", "tokens"] }
pnp = { git = "https://github.com/yarnpkg/pnp-rs.git", branch = "mael/pub-vpath" }
TypeScript에서 Rust로의 1:1 포팅이 아니라, Rust의 타입 시스템과 소유권 모델에 맞게 재설계된 것이 이 프로젝트들의 특징입니다.
4. 핵심 신기능
Yarn Switch: 버전 관리 도구
이 기능의 배경을 이해하려면, Corepack이라는 도구의 역할과 운명을 먼저 알아야 합니다.
Corepack은 Node.js에 실험적으로 포함된 패키지 매니저 버전 관리 도구였습니다.
package.json의 packageManager 필드를 읽어, 프로젝트가 요구하는 정확한 버전의 yarn이나 pnpm을 자동으로 설치·실행해주는 역할을 했습니다.
팀원 간 패키지 매니저 버전 불일치를 방지하는 핵심 인프라였죠.
다만 2024년 이후 Node.js 측에서 Corepack의 장기 포함 여부를 재검토하는 논의가 진행됐고, 생태계 차원에서 대체 수단 필요성이 커졌습니다. “실험적” 딱지를 떼지 못한 채 유지보수 부담만 커졌고, Node.js 코어에 패키지 매니저 관리 로직이 포함되는 것 자체에 대한 근본적인 의문이 제기된 것입니다.
Yarn 팀 입장에서 이것은 심각한 문제였습니다. Corepack 없이는 “이 프로젝트는 Yarn 4.12.0을 사용한다”는 보장이 사라지니까요.
Yarn Switch는 이런 공백을 메우기 위해 등장한 도구입니다.
rustup이나 nvm처럼 package.json의 packageManager 필드를 읽어 적절한 Yarn 버전을 자동으로 다운로드하고 실행합니다.
Corepack과 아이디어는 유사하지만, Yarn 팀이 직접 유지보수하며 Yarn에 최적화되어 있다는 점이 다릅니다.
구현 측면에서는 독립된 Rust 바이너리(zpm-switch crate)로, Yarn 본체와 별도로 설치·관리할 수 있습니다.
경량 바이너리가 ~/.yarn/switch/bin/yarn에 위치하면서 모든 yarn 호출을 가로채 적절한 버전으로 라우팅하는 구조입니다.
Lazy Installs: 자동 설치 감지
Yarn Berry의 Zero Installs는 혁신적인 아이디어였습니다.
.yarn/cache/에 모든 의존성을 zip 형태로 저장하고 Git에 커밋하면, yarn install 없이 바로 실행할 수 있습니다.
하지만 실무에서는 저장소 크기 증가, Git 성능 저하 등의 문제가 있었습니다.
Lazy Installs는 이 문제를 우아하게 해결합니다.
# 기존: install을 먼저 실행해야 함
$ yarn install
$ yarn run build
# Lazy Installs: run이 자동으로 install 여부를 감지
$ yarn run build # 필요하면 자동으로 install 실행
yarn run 같은 명령 실행 시, Yarn이 자동으로 설치 상태를 감지하고 필요한 경우에만 설치를 수행합니다.
Zero Installs처럼 의존성을 Git에 커밋할 필요 없이, 필요한 시점에 자동으로 설치가 이루어지는 것입니다.
project.rs의 lazy_install() 메서드가 이 기능의 진입점입니다.
5. Correctness: 정확성 우선 철학
Maël Nison은 Yarn 6의 우선순위를 명확히 했습니다.
Correctness > DX > Performance
성능이 아닌 정확성이 첫 번째입니다.
실제로 Yarn 6는 이미 Datadog에서 프로덕션 배포를 완료했습니다. 수천 개의 패키지를 가진 대규모 모노레포에서, Berry와 동일한 결과를 내는지를 검증한 것입니다.
코드에서도 이 철학이 드러납니다.
lockfile.rs의 from_legacy_berry_lockfile() 함수는 Berry 형식의 lockfile을 Yarn 6 형식으로 변환합니다.
심지어 from_pnpm_node_modules() 함수는 pnpm의 node_modules에서 lockfile을 재구성하는 기능까지 제공합니다.
// packages/zpm/src/lockfile.rs (구조)
pub fn from_legacy_berry_lockfile(/* ... */) -> Result<Lockfile> { /* ... */ }
pub fn from_pnpm_node_modules(/* ... */) -> Result<Lockfile> { /* ... */ }
기존 프로젝트에서 마이그레이션할 때 lockfile의 일관성이 깨지지 않도록, 레거시 호환성을 코드 레벨에서 보장하고 있는 것입니다.
6. 마이그레이션 가이드
버전 로드맵
| 버전 | 시기 | 상태 |
|---|---|---|
| Yarn 4.x | 현재 | stable (JavaScript) |
| Yarn 5.x | 2-3개월 내 | 추가 deprecated 기능 포함 |
| Yarn 6.x | 2026 Q3 이후 | Rust 기반 stable |
Yarn 5.x는 stable 버전 출시 후 약 30개월간 LTS 지원을 받으므로, 급하게 마이그레이션할 필요는 없습니다.
현재 남은 과제
Yarn 6 프리뷰에서 아직 미완성인 부분들이 있습니다.
- Windows 지원: 아직 완전하지 않음
- 대화형 명령어: 일부 interactive 명령이 미구현
- 잠금 파일 파싱 도구 호환성: 서드파티 도구와의 호환성
- 일부 테스트 및 명령어: 구현 진행 중
프리뷰 테스트 가이드
프리뷰 단계이므로, 우선 비프로덕션 환경에서 테스트하는 것을 권장합니다. 특히 CI 파이프라인에서의 설치 시간이 병목인 팀이라면, 벤치마크를 직접 돌려보는 것을 권장합니다.
7. 결론
정리하면 Yarn 6는 단순히 “더 빠른 yarn”으로만 보기는 어렵습니다.
SWC, Biome, Rspack에 이어 패키지 매니저 영역까지 네이티브 전환이 확장되는 흐름에서, Yarn 6는 기존 Yarn 생태계의 도메인 지식을 유지한 채 재구현을 진행한다는 점에서 의미가 큽니다.
Berry의 Resolver 인터페이스와 zpm의 Range enum을 나란히 놓고 보면,
같은 문제를 풀되 완전히 다른 도구와 사고방식으로 접근하는 과정이 보입니다.
Berry의 Project.install()이 3단계를 순차 실행하는 동안, zpm의 GraphTasks는 100개의 태스크를 동시에 스케줄링합니다.
Berry가 매번 YAML을 파싱하는 동안, zpm은 rkyv로 메모리에 직접 매핑합니다.
그 결과가 공식 벤치마크에서 68~82% 수준의 단축으로 보고됩니다.
Yarn 6의 stable 버전은 아직 몇 개월 남았지만, 지금 코드 구조를 읽어두면 전환 시점에 “왜, 어떻게 빨라졌는지”를 훨씬 빠르게 판단할 수 있을 것입니다.