뒤로가기

Yarn: 차세대 JavaScript 패키지 매니저와 Plug'n'Play 혁신

yarn

Yarn은 Facebook(현 Meta)이 npm의 한계를 극복하기 위해 2016년 출시한 JavaScript 패키지 매니저입니다. 많은 사람들이 Yarn을 "빠른 npm"으로 생각하지만, **Yarn의 진정한 목적은 속도가 아니라 프로젝트 관리의 안정성과 재현성(reproducibility)**입니다.

2020년 Yarn 2.0(Yarn Berry) 출시와 함께 Plug'n'Play(PnP) 방식이 도입되면서, Yarn은 node_modules를 제거하고 의존성 관리를 근본적으로 재설계했습니다. 이 글에서는 Yarn Classic과 Berry의 차이, PnP 메커니즘, 실전 활용 전략을 다룹니다.

Yarn의 탄생 배경

npm의 한계 (2016년 당시)

1. 비결정적 설치 (Non-deterministic Installs)

# 개발자 A의 머신
npm install express
# node_modules/express@4.17.1
 
# 개발자 B의 머신 (다음 날)
npm install express
# node_modules/express@4.17.2 (새 패치 버전 릴리스)

package.json에 ^4.17.0으로 명시되어 있으면, npm은 설치 시점에 따라 다른 버전을 설치할 수 있었습니다.

2. 느린 설치 속도

# npm (2016)
npm install lodash
# 1. 네트워크 요청
# 2. 압축 파일 다운로드
# 3. 압축 해제
# 4. node_modules 복사
# 평균 5-10초

3. 보안 취약점

npm은 패키지 설치 시 보안 검증이 부족했고, 체크섬 검증도 선택적이었습니다.

Yarn Classic의 해결책

1. yarn.lock: 결정적 의존성

# yarn.lock
express@^4.17.0:
  version "4.17.1"
  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==

Yarn은 프로젝트에 yarn.lock 파일을 생성하여, 모든 머신에서 동일한 버전을 설치하도록 보장했습니다.

2. 병렬 다운로드 및 캐싱

# Yarn Classic
yarn add lodash react axios
# 3개 패키지를 병렬로 다운로드
# 글로벌 캐시에 저장: ~/.yarn/cache
# 평균 2-3초 (npm 대비 3배 빠름)

3. 체크섬 검증

Yarn은 모든 패키지의 무결성을 yarn.lockintegrity 필드로 검증합니다.

Yarn Classic vs Yarn Berry (Yarn 2+)

아키텍처 비교

특징 Yarn Classic (v1) Yarn Berry (v2+)
node_modules 사용 (hoisted) PnP 모드: 제거node_modules 모드: 선택 가능
의존성 저장 디렉토리 구조 ZIP 파일 (.yarn/cache)
의존성 조회 Node.js 기본 해석기 .pnp.cjs 매핑 파일
버전 관리 전역 설치 필요 프로젝트별 버전 (yarn set version)
디스크 사용량 높음 (중복 저장) 낮음 (ZIP 압축 + 전역 캐시)
설치 속도 빠름 매우 빠름 (Zero-Install 가능)
호환성 높음 낮음 (PnP 비호환 패키지 존재)

Yarn Classic 구조

my-project/
├── node_modules/
   ├── express/         # Hoisted
   ├── react/           # Hoisted
   └── lodash/          # express의 의존성이지만 루트에 Hoisted
├── package.json
└── yarn.lock

Hoisting 문제:

  • lodashpackage.json에 없어도 import lodash from 'lodash' 가능 (Phantom Dependency)
  • 패키지 간 버전 충돌 시 복잡한 해결 규칙 필요

Yarn Berry 구조 (PnP 모드)

my-project/
├── .yarn/
   ├── cache/
   ├── express-npm-4.18.2-e0b5ea00d5-a7e37eafeb.zip
   ├── react-npm-18.2.0-1eae08fee2-88e38092da.zip
   └── lodash-npm-4.17.21-6382451519-eb835a2e51.zip
   ├── releases/
   └── yarn-4.5.3.cjs  # 프로젝트별 Yarn 실행 파일
   └── unplugged/          # Native 모듈용
├── .pnp.cjs                # 의존성 맵
├── .pnp.loader.mjs         # ESM 로더
├── .yarnrc.yml             # 설정 파일
├── package.json
└── yarn.lock

Plug'n'Play (PnP) 메커니즘

node_modules의 근본적 문제

1. I/O 오버헤드

# node_modules 방식 (Yarn Classic/npm)
$ ls -la node_modules | wc -l
157832 # 15만 개 이상의 파일
 
# 설치 시간: 압축 해제 + 디스크 쓰기
$ time npm install
real    0m32.451s

2. 비효율적인 의존성 조회

// require('express')를 실행하면 Node.js는:
// 1. ./node_modules/express 확인
// 2. ../node_modules/express 확인
// 3. ../../node_modules/express 확인
// ... (최상위 디렉토리까지 반복)

평균적으로 require() 호출당 여러 번의 파일 시스템 조회가 발생합니다.

PnP의 혁신적 접근

1. .pnp.cjs: 의존성 맵

// .pnp.cjs (간소화된 예시)
const packageRegistry = new Map([
  ["express", [
    ["npm:4.18.2", {
      packageLocation: "./.yarn/cache/express-npm-4.18.2-a7e37eafeb.zip/node_modules/express/",
      packageDependencies: new Map([
        ["body-parser", "npm:1.20.1"],
        ["cookie", "npm:0.5.0"],
      ]),
    }],
  ]],
  ["react", [
    ["npm:18.2.0", {
      packageLocation: "./.yarn/cache/react-npm-18.2.0-88e38092da.zip/node_modules/react/",
      packageDependencies: new Map([
        ["loose-envify", "npm:1.4.0"],
      ]),
    }],
  ]],
]);
 
// Yarn이 제공하는 커스텀 resolver
function require(moduleName) {
  const pkg = packageRegistry.get(moduleName);
  return loadFromZip(pkg.packageLocation);
}

2. 동작 원리

// 기존 방식 (node_modules)
require('express');
// Node.js가 파일 시스템을 순회하며 찾음 (느림)
 
// PnP 방식
require('express');
// 1. .pnp.cjs의 packageRegistry에서 O(1) 조회
// 2. ZIP 파일 경로 직접 접근
// 3. ZIP에서 모듈 로드 (느림 → 해결: .yarn/unplugged)

3. 성능 비교

# 벤치마크: Express 프로젝트 (53개 의존성)
 
npm (v8):                    32.4초
Yarn Classic (v1.22):        14.2초
Yarn Berry (node_modules):   12.8초
Yarn Berry (PnP):            2.3초  # 14배 빠름
Yarn Berry (PnP + cache):    0.4초  # 80배 빠름

PnP의 장점

1. Phantom Dependency 방지

// package.json에 lodash 미선언
 
// Yarn Classic/npm: 동작 (잘못됨)
import _ from 'lodash'; // OK (express가 가져옴)
 
// Yarn Berry PnP: 에러 (올바름)
import _ from 'lodash';
// Error: 'lodash' is not declared in package.json

2. 디스크 공간 절약

# 10개 프로젝트에서 동일한 의존성 사용 시
npm/Yarn Classic:  10 × 300MB = 3GB
Yarn Berry PnP:    1 × 180MB (ZIP) = 180MB  # 17배 절약

3. Zero-Install

# .yarn/cache를 Git에 커밋
git add .yarn/cache
git commit -m "Add dependencies to repository"
 
# 다른 개발자가 클론 후
git clone https://github.com/user/project
cd project
yarn  # 설치 없이 바로 사용 가능 (0.1초)

PnP의 단점 및 호환성 문제

1. 네이티브 모듈 비호환

# node-gyp로 빌드되는 패키지
node-sass, bcrypt, sqlite3, canvas
 
# 해결: unplugged 모드
# .yarnrc.yml
packageExtensions:
  "node-sass@*":
    unplugged: true

2. 동적 require() 미지원

// 동작하지 않음
const moduleName = 'express';
require(moduleName);  // Error: Cannot resolve dynamic require
 
// 해결: package.json에 명시적 선언
{
  "dependencies": {
    "express": "^4.18.0"
  }
}

3. IDE 지원 부족 (과거)

VS Code, WebStorm 등은 2021년 이후 PnP를 공식 지원합니다.

// .vscode/settings.json
{
  "typescript.tsdk": ".yarn/sdks/typescript/lib",
  "eslint.nodePath": ".yarn/sdks"
}
 
// SDK 생성
yarn dlx @yarnpkg/sdks vscode

Corepack: Yarn 버전 관리

Corepack이란?

Node.js 16.9+ 버전에 포함된 패키지 매니저의 버전 관리 도구입니다.

# Node.js에 포함된 Yarn 확인
which yarn
# /usr/local/bin/yarn (Corepack shim)
 
# Corepack 활성화
corepack enable
 
# 프로젝트별 Yarn 버전 설정
# package.json
{
  "packageManager": "yarn@4.5.3"
}
 
# Yarn 명령 실행 시 자동으로 4.5.3 다운로드 및 사용
yarn install

Yarn Classic vs Berry 전환

프로젝트별 버전 설정:

# Yarn Berry로 업그레이드
yarn set version berry
# 또는 특정 버전
yarn set version 4.5.3
 
# .yarn/releases/yarn-4.5.3.cjs 생성
# .yarnrc.yml 생성
yarnPath: .yarn/releases/yarn-4.5.3.cjs
 
# 다시 Classic으로 되돌리기
yarn set version classic

프로젝트 A (Yarn Berry):

cd project-a
cat .yarnrc.yml
# yarnPath: .yarn/releases/yarn-4.5.3.cjs
yarn --version  # 4.5.3

프로젝트 B (Yarn Classic):

cd project-b
cat package.json
# "packageManager": "yarn@1.22.19"
yarn --version  # 1.22.19

실전 활용 가이드

Yarn Berry 마이그레이션 체크리스트

1. 프로젝트 준비

# 1단계: Yarn Berry 설치
yarn set version berry
 
# 2단계: PnP 모드 활성화 (.yarnrc.yml 자동 생성)
# nodeLinker: pnp  # 기본값
 
# 3단계: 의존성 재설치
rm -rf node_modules
yarn install
 
# 4단계: IDE SDK 설정
yarn dlx @yarnpkg/sdks vscode

2. TypeScript 설정

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "types": ["node"]
  },
  "ts-node": {
    "transpileOnly": true,
    "require": ["./.pnp.cjs"]  // PnP 로더
  }
}

3. ESLint 설정

# ESLint PnP 플러그인 설치
yarn add -D eslint-plugin-pnp
 
# .eslintrc.js
module.exports = {
  extends: ['plugin:pnp/recommended'],
};

4. 호환성 문제 해결

# .yarnrc.yml
# 비호환 패키지를 node_modules 모드로 설치
packageExtensions:
  "react-native@*":
    peerDependencies:
      react: "*"
  "node-sass@*":
    unplugged: true
 
# 또는 전역적으로 node_modules 모드 사용
nodeLinker: node-modules

Zero-Install 전략

장점:

  • CI/CD에서 yarn install 생략 → 빌드 시간 단축
  • 의존성 버전이 Git 커밋에 고정됨 → 재현성 보장

설정:

# .yarnrc.yml
enableGlobalCache: false  # 프로젝트별 캐시 사용
 
# .gitignore
.yarn/*
!.yarn/cache      # 캐시는 커밋
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

고려사항:

# 대용량 프로젝트의 캐시 크기
du -sh .yarn/cache
# 180MB (소규모)
# 2.5GB (모노레포)
 
# Git LFS 사용 권장 (대규모 프로젝트)
git lfs install
git lfs track ".yarn/cache/**/*.zip"

Workspace를 이용한 모노레포

디렉토리 구조:

my-monorepo/
├── packages/
   ├── web/
   ├── package.json  # "name": "@myapp/web"
   └── src/
   ├── mobile/
   ├── package.json  # "name": "@myapp/mobile"
   └── src/
   └── shared/
       ├── package.json  # "name": "@myapp/shared"
       └── src/
├── package.json
└── yarn.lock

루트 package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "yarn workspaces foreach -A run build",
    "test": "yarn workspaces foreach -A run test"
  }
}

패키지 간 의존성:

// packages/web/package.json
{
  "name": "@myapp/web",
  "dependencies": {
    "@myapp/shared": "workspace:*"  // 같은 모노레포의 패키지
  }
}

Workspace 명령어:

# 특정 workspace에 패키지 추가
yarn workspace @myapp/web add react
 
# 모든 workspace에서 스크립트 실행
yarn workspaces foreach run build
 
# 의존성 그래프 시각화
yarn workspaces list --json

성능 벤치마크

설치 속도 비교

테스트 환경: React 프로젝트 (134개 의존성, macOS M1)

패키지 매니저 Cold Install Warm Install (캐시)
npm v8 32.4s 18.2s
Yarn Classic 14.2s 8.7s
pnpm v7 11.3s 4.2s
Yarn Berry (node_modules) 12.8s 5.1s
Yarn Berry (PnP) 2.3s 0.4s
Yarn Berry (Zero-Install) - 0.1s

디스크 사용량 비교

10개 프로젝트 (각 134개 의존성):

npm:           3.2GB  (100%)
Yarn Classic:  3.1GB  (96.8%)
pnpm:          680MB  (21.2%)  # 하드링크
Yarn Berry:    520MB  (16.2%)  # ZIP 압축

모노레포 빌드 시간

Turborepo + Yarn Berry Workspace (50개 패키지):

# Cold build
Yarn Classic: 145s
Yarn Berry:   92s   # 1.6배 빠름
 
# Warm build (캐시)
Yarn Classic: 78s
Yarn Berry:   23s   # 3.4배 빠름

트러블슈팅

1. PnP 호환성 오류

증상:

Error: Cannot find module 'some-package'

해결:

# .yarnrc.yml - 특정 패키지만 unplugged
packageExtensions:
  "some-package@*":
    unplugged: true
 
# 또는 node_modules 모드로 전환
nodeLinker: node-modules

2. TypeScript 경로 인식 오류

증상:

Cannot find module '@/components/Button' or its corresponding type declarations.

해결:

# SDK 재생성
yarn dlx @yarnpkg/sdks vscode
 
# VS Code 재시작
# Cmd+Shift+P → "TypeScript: Select TypeScript Version" → "Use Workspace Version"

3. Next.js 13+ App Router 오류

증상:

Module not found: Can't resolve 'react/jsx-runtime'

해결:

// package.json
{
  "packageManager": "yarn@4.5.3",
  "dependencies": {
    "react": "^18.2.0"
  }
}
# .yarnrc.yml
nodeLinker: node-modules  # Next.js는 PnP 비호환 (2024년 기준)

4. CI/CD 캐시 설정

GitHub Actions:

# .github/workflows/ci.yml
- name: Setup Node.js
  uses: actions/setup-node@v3
  with:
    node-version: 18
    cache: 'yarn'  # Yarn Berry 자동 인식
 
- name: Enable Corepack
  run: corepack enable
 
- name: Install dependencies
  run: yarn install --immutable  # yarn.lock 수정 금지

Zero-Install 사용 시:

- name: Install dependencies
  run: |
    # .yarn/cache가 Git에 있으므로 즉시 사용 가능
    yarn --version

언제 Yarn Berry를 사용해야 할까?

Yarn Berry 추천 상황

[권장] 모노레포 프로젝트

  • Workspace 기능이 강력하고 빌드 속도가 빠름

[권장] CI/CD 시간 최적화가 중요한 경우

  • Zero-Install로 설치 단계를 완전히 제거 가능

[권장] 디스크 공간이 제한적인 환경

  • ZIP 압축으로 60-80% 공간 절약

[권장] 의존성 보안이 중요한 경우

  • Phantom Dependency 방지로 공격 표면 감소

Yarn Classic 또는 다른 도구 추천 상황

[주의] React Native 프로젝트

  • Metro bundler가 PnP를 지원하지 않음 (2024년 기준)

[주의] 레거시 프로젝트

  • 동적 require() 사용이 많은 경우 마이그레이션 비용 높음

[주의] 네이티브 모듈 의존성이 많은 경우

  • unplugged 설정이 복잡함

결론

Yarn은 단순히 "빠른 npm"이 아닙니다. Yarn Classic은 결정적 의존성 관리를, Yarn Berry는 PnP를 통한 근본적인 패러다임 전환을 목표로 합니다.

핵심 요약:

  1. Yarn Classic: npm 대비 빠르고 안정적이지만, node_modules의 구조적 한계는 동일
  2. Yarn Berry PnP: node_modules 제거, 2-80배 빠른 설치, Phantom Dependency 방지
  3. Corepack: 프로젝트별 Yarn 버전 관리로 유연한 전환 가능
  4. Zero-Install: CI/CD 최적화 및 재현성 보장
  5. Workspace: 모노레포에 최적화된 강력한 도구

마이그레이션 전략:

# 1단계: 기존 프로젝트에서 테스트
yarn set version berry
yarn install
npm run build  # 정상 동작 확인
 
# 2단계: 호환성 문제 해결
# .yarnrc.yml에서 nodeLinker: node-modules 사용
 
# 3단계: PnP 전환 (선택)
# nodeLinker: pnp
# IDE SDK 설정
# 팀원 교육
 
# 4단계: Zero-Install (선택)
# enableGlobalCache: false
# .yarn/cache 커밋

Yarn Berry는 초기 학습 곡선이 있지만, 장기적으로 프로젝트 관리의 안정성과 성능을 크게 향상시킵니다. 특히 모노레포 환경에서는 Yarn Berry + Workspace의 조합이 현재 가장 강력한 도구입니다.

관련 아티클