Yarn v1 → pnpm 마이그레이션 결과 및 3-way 비교

대상: coinness-web (단일 패키지, Vite 6 + React 18 SPA) 전환: Yarn v1 (1.22) → pnpm 11.18.0 (corepack) 작성일: 2026-07-31 비교군: Yarn v1 / Yarn Berry 4.18(node-modules 링커) / pnpm 11.18

1. 배경

성능(설치 속도·디스크)·재현성 개선을 위해 v1 이후 패키지 매니저를 검토했다. Berry(node-modules) 실험에 이어, 분석 1순위였던 pnpm을 별도 브랜치(chore/pnpm)에서 v1 베이스 위에 전환·검증했다. pnpm의 엄격한 심링크 node_modules가 이 저장소의 vite 설정(manualChunks, resolve.alias) 및 암묵적 의존성과 충돌하는지가 핵심 관전 포인트였다.

2. 실측 벤치마크 (3-way)

측정 환경: Apple Silicon(macOS), 각 매니저의 글로벌 캐시/스토어 워밍 상태, 라이프사이클 스크립트 제외, 동일 의존성.

시나리오 Yarn v1 Yarn Berry 4.18 pnpm 11.18
Clean install (node_modules 없음) 27.3s 19.2s 16.6s
No-op 재설치 (변경 없음) 0.23s 1.24s 0.25s
node_modules du 802M 781M 620M*

* pnpm의 node_modules는 전역 content-addressable 스토어로의 하드링크다. du가 표시하는 620M는 스토어와 공유되는 블록이므로, 프로젝트/브랜치를 추가해도 실제 증가 디스크는 거의 0이다. 반면 v1·Berry는 프로젝트마다 파일을 복사한다.

해석

  • pnpm이 세 시나리오 모두 우위. Clean install 최速(16.6s, v1 대비 −39%), no-op은 v1과 동급(0.25s, Berry의 1.24s보다 빠름), 디스크는 하드링크 공유로 사실상 최소.
  • 순수 성능/디스크 관점에서 pnpm이 v1·Berry를 모두 앞선다. 대신 아래의 엄격성 비용(phantom 의존성 수정·설정 이전)을 치른다.

3. pnpm 전환에서 발생한 이슈 (엄격성 비용)

pnpm의 non-flat 심링크 레이아웃은 v1·Berry가 호이스팅으로 덮던 문제들을 빌드 실패로 표면화했다.

3-1. Phantom dependency (직접 의존성 누락) — 4건

코드가 직접 import하지만 package.json에 선언되지 않아(전이 의존성에 의존) pnpm에서 해석 실패:

패키지 import 위치(예) 실제 공급원 조치
@radix-ui/react-slot components/ui/button/BaseButton.tsx 다른 @radix-ui 패키지 dependencies 추가 (^1.2.3)
react-router pages/**/*Layout.tsx 등 6곳 react-router-dom dependencies 추가 (^6.8.1)
quill Opinion/**, services/Community/opinion.ts react-quill dependencies 추가 (^1.3.7)
@types/quill (타입 전용) react-quill의 전이 @types devDependencies 추가 (^1.3.10)

이 4건은 원래 직접 선언됐어야 할 의존성이다. pnpm 전환이 정합성 문제를 드러낸 것으로, 수정 자체가 코드 위생 개선이다.

3-2. pnpm 11 설정 위치 변경

  • pnpm 11은 package.json의 pnpm 필드를 더 이상 읽지 않는다(overrides 포함 무시, WARN 발생). 설정은 pnpm-workspace.yaml 로 이전해야 한다.
  • 의존성 build script는 기본 차단(ERR_PNPM_IGNORED_BUILDS) → pnpm-workspace.yaml의 allowBuilds 로 승인 필요(esbuild·msw·oxc-resolver·protobufjs).
  • pnpm-workspace.yaml이 존재하면 워크스페이스로 간주되어 packages: 필드가 필수다. 단일 패키지는 packages: ['.']로 선언.

3-3. 락파일 재해석

pnpm import는 v1 yarn.lock을 1:1로 옮기지 않고 범위 내 최신으로 재해석했다(예: piexif-ts 2.0.16→2.1.0, react-select 5.7→5.8). 실사용상 문제는 없으나, 정확한 버전 동결이 필요하면 별도 핀 관리가 필요하다.

3-4. 실행 전 deps 상태 검사

pnpm은 pnpm run/exec 전 node_modules 정합성을 검사해, ignored-builds·미승인 상태에선 명령 자체를 막는다(설정을 맞추면 해소).

4. 검증 결과 (로컬)

항목 결과
pnpm install ✅ build script 승인 후 정상 (esbuild 등 빌드)
pnpm build ✅ 22.2s, manualChunks vendor 그룹 11종 전부 유지(심링크 경로에도 /node_modules/<pkg>/ 세그먼트 보존), piexif resolve.alias 정상
pnpm lint ✅ eslint 실행 (경고 3건은 마이그레이션 무관 기존 코드)
pnpm test:unit ✅ 172/172 통과 (9 suites)
CI 시뮬레이션 (--frozen-lockfile) ✅ 락파일 정합, exit 0

5. 변경 사항

파일 변경
package.json packageManager: pnpm@11.18.0, engines.node, phantom-dep 4건 추가, pnpm/resolutions 필드 제거
pnpm-lock.yaml (신규) pnpm import로 생성 (yarn.lock 삭제)
pnpm-workspace.yaml (신규) packages: ['.'], overrides.esbuild, allowBuilds
.gitignore /.pnpm-store 추가
.husky/pre-commit yarn lint-staged/test:related → pnpm ...
scripts/chromatic-deploy.sh yarn build-storybook/chromatic → pnpm ...
.gitlab-ci.yml offline-mirror 제거 → corepack enable + pnpm install --frozen-lockfile --store-dir .pnpm-store, 캐시 .pnpm-store/, 키 pnpm-lock.yaml, job은 pnpm build/pnpm test:ci

6. 결론: v1 vs Berry vs pnpm

관점 Yarn v1 Yarn Berry(nm) pnpm
Clean install 속도 느림 중 최速
일상 no-op 속도 빠름 느림 빠름
실질 디스크 큼 큼 최소(하드링크)
phantom-dep 방지 없음 없음 강함
마이그레이션 비용 — 낮음 중(의존성 수정·설정 이전)
툴 유지보수 EOL 활발 활발
  • 성능·디스크·위생이 목표라면 pnpm이 최선. 세 시나리오 모두에서 v1을 앞서고, phantom-dep를 원천 차단한다.
  • 비용은 초기 1회성이다: 직접 의존성 4건 추가 + 설정을 pnpm-workspace.yaml로 이전. 이후로는 이득만 남는다.
  • 반면 최소 변경·최대 호환이 목표면 Berry(node-modules)가 안전하나 성능 이득은 pnpm보다 작다.

권장: 성능/디스크/위생 개선이 전환 동기이므로 pnpm 채택. 단, 병합 전 CI 파이프라인에서 install/build/test 통과를 실제로 확인하고, phantom-dep 수정이 런타임 회귀를 만들지 않는지 주요 화면을 점검할 것.