yarn

younghyun·2023년 3월 6일
1

npm과 yarn

npm과 yarn은 자바스크립트 런타임 환경인 노드(Node.js) 패키지 관리자. 전 세계의 개발자들이 자바스크립트로 만든 다양한 패키지를 npm 온라인 데이터베이스 (opens new window)에 올리면 npm, yarn과 같은 패키지 관리자를 통해 설치 및 삭제 가능함. 그리고 명령 줄 인터페이스(Command-line interface, CLI)를 통해 패키지 설치 및 삭제뿐 아니라 패키지 버전 관리, 의존성 관리도 편리하게 할 수 있음.

npm

npm은 노드 패키지 매니저(Node Package Manager) 줄임말로 노드 설치할 때 자동 설치되는 기본 패키지 관리자.
1. 온라인 플랫폼 (opens new window). 사람들이 노드 패키지를 만들고, 업로드하고, 공유할 수 있는 공간으로 누구나 온라인 플랫폼(npm 레지스트리)에 게시된 패키지 사용할 수 있음.
2. 두 번째 역할은 명령 줄 인터페이스. 온라인 플랫폼과 상호 작용하기 위해 명령 줄 인터페이스를 사용하며 패키지 설치 및 제거 가능.

설치

노드를 다운로드하면 npm이 시스템에 자동으로 설치됨. 노드 설치 후 다음과 같은 명령을 통해 npm이 설치되었는지 확인할 수 있음.

node -v
npm -v

yarn

2016년 페이스북에서 개발한 패키지 관리자. 리액트(React)와 같은 프로젝트를 진행하며 겪었던 어려움을 해결하기 위해 개발되었고, npm 레지스트리와 호환하면서 속도나 안정성 측면에서 npm보다 향상됨. 2016년 페이스북이 공개한 아티클 (opens new window)

설치

yarn은 npm 통해 설치.

npm install yarn --global

맥 사용자라면 brew 통해 설치할 수도 있음.

brew update
brew install yarn

yarn.lock

설치된 모듈 버전을 저장해 어디서나 같은 버전과 구조 의존성 가지게 함.
Yarn에서는 자동으로 yarn install 때 마다 yarn.lock이 생성.
package-lock.json와 비슷한 기능.

npm과 yarn 차이점

속도

npm과 yarn의 주요 차이점 중 하나는 패키지 설치 프로세스를 처리 방법. npm은 패키지를 한 번에 하나씩 순차적으로 설치. 그에 비해 yarn은 여러 패키지를 동시에 가져오고 설치하도록 최적화되어 있어 패키지 설치 속도 측면에서 yarn이 npm보다 빠름. 특히 yarn은 캐싱을 이용해서 패키지 설치가 더 빠름. Yarn은 설치한 패키지를 유저 디렉토리에 저장해서 캐싱. Yarn의 캐싱 디렉토리 경로는 아래와 같음.

$ yarn cached dir
/Library/Caches/Yarn/v6

첫번째 설치에서는 npm과 비슷한 성능을 보이지만 두번째 install 부터는 npm보다 훨씬 빠르게 패키지를 설치.

보안

yarn은 보안 측면에서 npm보다 더 안전한 것으로 알려져 있음. npm은 자동으로 패키지에 포함된 다른 패키지 코드를 실행. 이로 인해 보안 시스템에 몇 가지 취약성이 발생하며 나중에 심각한 문제가 발생할 수 있음. 반면에 yarn은 yarn.lock 또는 package.json파일에 있는 파일만을 설치. 보안은 yarn의 핵심 기능 중 하나이지만 최근 npm 업데이트에서 npm 보안 업데이트도 크게 향상됨.

Yarn(패키지 설치) 작동원리

Yarn (패키지 매니저)는 의존 패키지들을 어떻게 관리하는가?

우선 프로젝트의 메타 정보는 package.json 파일을 통해 관리된다. package.json 에는 해당 프로젝트가 의존하는 모든 패키지 이름과 버전이 나열되어있는데, dependencies 항목에는 일반적으로 설치되어야 할 패키지가, 그리고 devDependencies 항목에는 개발용 패키지가 명시되어있다. package.json 는 아래와 같이 생겼다.

{
  "name": "test"
  "version": "1.0.0",
  "private": true,
  "description": "Short Decsription of the project",
  "scripts": {
    "start": "NODE_ENV=production next start --port 8000",
    ... 
  },
  "dependencies": {
    "react": "17.0.1",
    ...
  },
   "devDependencies": {
     "@types/react": "^17.0.0",
     ...
   }
  
}

이렇게 설치가 필요한 패키지들이 package.json 파일에 등록되어 있으면, 프로젝트에 협업중인 개발자들은 yarn 또는 yarn install 명령어 하나로 모든 패키지들을 한번에 설치할 수 있음.

yarn install을 치면, package.json 에 등록되어 있는 모든 패키지를 yarn registry에서 다운받아 node_modules 디렉터리에 저장함. 이때, node_modules 디렉터리에는 package.json 에 적혀있는 패키지 이외에도 여러가지 패키지들이 설치되어 있음. 해당 패키지들은 프로젝트에 직접적으로 필요하지는 않지만, 간접적으로 의존하는 패키지들. ls node_modules 커맨드로 설치된 패키지들을 모두 볼 수 있음.

 ls node_modules
react
react-colorful
react-copy-to-clipboard
react-datepicker
..

yarn 으로 패키지를 설치하면, yarn.lock 라는 패키지 잠금 파일이 생성됨. 패키지 잠금 파일에는 프로젝트에 패키지를 최초로 추가할 당시 어떤 버전이 설치되었는지 하나하나 상세히 기록한 파일. 예시로, 첫 프로젝트에서 yarn install을 치면 아래와 같은 yarn.lock 파일이 생김.

주의: yarn.lock 파일은 패키지 설치 시 자동 생성되는 파일이므로, 임의로 조작해서는 안됨.

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

"@gar/promisify@^1.0.1":
  version "1.1.2"
  resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz"
  integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==


"@jest/types@^26.6.2":
  version "26.6.2"
  resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz"
  integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
  dependencies:
    "@types/istanbul-lib-coverage" "^2.0.0"
    "@types/istanbul-reports" "^3.0.0"
    "@types/node" "*"
    "@types/yargs" "^15.0.0"
    chalk "^4.0.0"

..

위처럼 yarn.lock 파일이 생성되면, 그 이후로는 yarn install 커맨드를 실행해도 yarn registry에 있는 최신 버전을 설치하지 않음. 대신에, 항상 yarn.lock 파일에 명시되어 있는 버전으로 패키지를 설치를 해주기 때문에, 설치 시점에 상관없이 항상 동일한 버전의 패키지를 설치할 수 있음. (패키지 버전 문제 발생을 최소화 할 수 있음.)

즉, package.jsonyarn.lock 파일만 있으면 해당 프로젝트가 의존하고 있는 모든 패키지를 설치할 수 있음. 따라서 용량이 큰 node_modules 디렉터리는 Git 저장소에 올라가지 않도록 .gitignore 파일에 추가할 수 있음.

명령어

Yarn 설치

macOS에서는 brew로 설치가 가능.

brew install yarn

npm 을 사용한다면 npm으로도 설치할 수 있음.

npm install -g yarn

yarn이 어디에 설치되었는지 확인.

$ which yarn
usr/local/bin/yarn
yarn init

프로젝트에 필요한 기본적인 정보를 입력하고 yarn init을 하면, package.json 파일이 생성됨.

$ mkdir test
$ cd test
$ yarn init
yarn init v1.3.2
question name (test):
question version (1.0.0):
question description:
question entry point (index.js):
..
yarn install

프로젝트를 시작할 때 yarn 혹은 yarn install 로 package.json 파일에 정의된 모든 dependencies를 설치할 수 있음.

$ yarn
$ yarn install

해당 명령어를 치면, 로컬의 node_modules 폴더에 의존 패키지를 설치하거나 업데이트.
강제로 모든 패키지를 다시 다운로드 하고 싶을때는 --force 플래그를 추가.

$ yarn install --force
yarn remove

해당 명령어는 특정 패키지를 프로젝트에서 제거한다는 것을 의미.
package.json과 yarn.lock에서 동시에 제거.
또한, dependencies, devDependencies등 모든 타입에서 패키지가 삭제.

$ yarn remove [package]

yarn remove를 실행하면 package.json과 yarn.lock을 모두 업데이트. 따라서, 한 개발자가 자신의 로컬 환경에서 패키지를 지우면 프로젝트에서 협업하는 다른 동료들도 해당 패키지를 삭제하고, 동일한 의존 패키지(dependencies)를 사용.

yarn check

yarn check는 특정 버전 패키지의 의존 패키지(dependencies)들이 프로젝트에서 유효한지 체크. 즉, 현재의 package.json이 yarn.lock과 일치하는지 확인.

$ yarn check
yarn check v1.22.4
warning "@babel/helper-replace-supers#@babel/traverse@^7.13.0" could be deduped from "7.14.2" to "@babel/traverse@7.14.2"
error "eslint-plugin-react-hooks#eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" doesn't satisfy found match of "eslint@7.23.0"
...

info Found 7 warnings.
error Found 2 errors.
info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.

위 예시처럼 yarn check 명령 후에 warning, error가 뜬다면 의존 패키지의 버전을 조정할 필요가 있음.

더 많은 명령어를 알고 싶다면 공식문서를 참고하거나, yarn help를 치면됨.

$ yarn help

주의사항

패키지 관리자로 yarn 과 npm을 섞어서 사용하는 경우가 있음. 하지만 yarn 과 npm은 패키지 관리 방식이 다르기 때문에 충돌이 날 수 있으므로 가급적이면 혼용하지 않는게 좋음.

warning: package-lock.json found. Your
project contains lock files generated by
tools
other than Yarn. It is advised not to mix package managers in order to avoid resolution 
inconsistencies caused by unsynchronized lock files. To clear this warning, remove
package-lock.json.

npm: 각 설치한 패키지별로 서브패키지를 이루는 형식. 각 설치한 패키지의 독립성이 보장되지만 패키지 중복으로 인한 크기가 전체적으로 커짐.

yarn: 설치한 패키지와 종속되는 패키지를 공통적으로 사용할 때 일렬로 나열한 뒤 설치 패키지로 링크하는 방식. 패키지 중복이 제거되어 적은 용량으로 빨리 실행되나, 네이티브 및 yarn을 고려하지 않은 버전 관리로 패키지 충돌이 있을 수 있음.

lock 파일이 둘 다 있더라도 에러가 안날 수 있지만, 동일한 패키지 관리자로 진행하는 게 패키지 충돌 오류를 최소화 할 수 있음. 처음에 yarn을 사용했다면 끝까지 yarn을 쓰거나, npm을 썼다면 계속 npm을 사용하는 걸 추천.

결론

npm과 yarn은 모두 종속성을 관리하고 패키지를 관리하기 좋은 툴. 둘 다 지속적으로 관리되고 있으며 폭넓은 사용자 커뮤니티를 가지고 있고, 업데이트를 통해 추가된 기능 덕분에 거의 차이 나지 않게 됨. 결론적으로 둘 중에 무엇을 선택해야 할지는 개인의 취향, 성능(패키지 설치 속도), 커뮤니티에 따라 달라질 수 있을 것 같음.

yarn berry

Yarn Berry는 Yarn v1의 주요 개발자인 Maël Nison 씨가 만듦.
Yarn은 1.22를 마지막으로 Classic버전의 관리를 중단.
2.X 이후부터는 Yarn Modern(berry)이라 부르고 새로운 구조로 패키지를 관리.

  1. npm 또는 yarn classic 버전과 다르게, 모듈들을 node_modules에다가 관리하지 않고, 압축 파일을 통한 패키지 의존성 관리 .yarn/cache폴더에 두고, 해당 모듈들을 .pnp.cjs, .pnp.loader.mjs를 통해서 가져옴.

  2. pnp를 통한 zero-install.

2020년 1월 25일부터 정식 버전(v2)가 출시 되어, 현재는 Babel과 같은 큰 오픈소스 레포지토리에서도 채택하고 있음. Yarn Berry는 GitHub yarnpkg/berry 레포지토리에서 소스코드가 관리되고 있음.

NPM 문제점

비효율적인 의존성 검색

NPM은 파일 시스템 이용하여 의존성 관리함. 익숙한 node_modules 폴더를 이용하는 것이 특징. 이렇게 관리하면 의존성 검색은 비효율적으로 동작.

예를 들어, /Users/toss/dev/toss-frontend-libraries 폴더에서 require() 문을 이용하여 react 패키지를 불러오는 상황가정.

라이브러리를 찾기 위해 순회하는 디렉토리의 목록을 확인하려고 할 때, Node.js에서 제공하는 require.resolve.paths() 함수를 사용할 수 있음. 이 함수는 NPM이 검색하는 디렉토리 목록 반환.

$ node
Welcome to Node.js v12.16.3.
Type ".help" for more information.
> require.resolve.paths('react')
[
  '/Users/toss/dev/toss-frontend-libraries/repl/node_modules',
  '/Users/toss/dev/toss-frontend-libraries/node_modules',
  '/Users/toss/node_modules',
  '/Users/node_modules',
  '/node_modules',
  '/Users/toss/.node_modules',
  '/Users/toss/.node_libraries',
  '/Users/toss/.nvm/versions/node/v12.16.3/lib/node',
  '/Users/toss/.node_modules',
  '/Users/toss/.node_libraries',
  '/Users/toss/.nvm/versions/node/v12.16.3/lib/node'
]

목록에서 확인할 수 있는 것처럼, NPM은 패키지를 찾기 위해서 계속 상위 디렉토리의 node_modules 폴더를 탐색함. 따라서 패키지를 바로 찾지 못할수록 readdir, stat과 같은 느린 I/O 호출이 반복됨. 경우에 따라서는 I/O 호출이 중간에 실패하기도 함.

환경에 따라 달라지는 동작

NPM은 패키지를 찾지 못하면 상위 디렉토리의 node_modules 폴더를 계속 검색. 이 특성 때문에 어떤 의존성을 찾을 수 있는지는 해당 패키지의 상위 디렉토리 환경에 따라 달라짐.

예를 들어, 상위 디렉토리가 어떤 node_modules를 포함하고 있는지에 따라 의존성을 불러올 수 있기도 하고, 없기도 함. 다른 버전의 의존성을 잘못 불러올 수 있는 여지도 존재.

환경에 따라 동작이 변하는 것은 좋지 않음.

비효율적인 설치

NPM에서 구성하는 node_modules 디렉토리 구조는 매우 큰 공간을 차지함. 일반적으로 간단한 CLI 프로젝트도 수백 메가바이트의 node_modules 폴더가 필요함. 용량만 많이 차지할 뿐 아니라, 큰 node_modules 디렉토리 구조를 만들기 위해서는 많은 I/O 작업이 필요함.

node_modules 폴더는 복잡하기 때문에 설치 유효성을 검증하기 어려움.
예를 들어, 수백 개의 패키지가 서로를 의존하는 복잡한 의존성 트리에서 node_modules 디렉토리 구조는 깊어짐.
이렇게 깊은 트리 구조에서 의존성이 잘 설치되어 있는지 검증하려면 많은 수의 I/O 호출이 필요함. 일반적으로 디스크 I/O 호출은 메모리의 자료구조를 다루는 것보다 훨씬 느림. 이런 문제로 인해 Yarn v1이나 NPM은 기본적인 의존성 트리의 유효성까지만 검증하고, 각 패키지의 내용이 올바른지는 확인하지 않음.

유령 의존성

NPM 및 Yarn v1에서는 중복해서 설치되는 node_modules를 아끼기 위해 (속도 문제 개선) 끌어올리기(Hoisting) 기법 등 최적화 알고리즘을 도입.

예를 들어, 의존성 트리가 왼쪽의 모습을 하고 있다고 가정.

왼쪽 트리에서 [A (1.0)]과 [B (1.0)] 패키지는 두 번 설치되므로 디스크 공간을 낭비. NPM과 Yarn v1에서는 디스크 공간을 아끼기 위해 원래 트리의 모양을 오른쪽 트리처럼 바꿈.

오른쪽 트리로 의존성 트리가 바뀌면서 package-1 에서는 원래 require() 할 수 없었던 [B (1.0)] 라이브러리를 불러올 수 있게 됨.

이렇게 끌어올리기에 따라 직접 의존하고 있지 않은 라이브러리를 require() 할 수 있는 현상을 유령 의존성(Phantom Dependency)이라고 부름.

유령 의존성 현상이 발생할 때, package.json에 명시하지 않은 라이브러리를 조용히 사용할 수 있게 됨. 다른 의존성을 package.json 에서 제거했을 때 소리없이 같이 사라지기도 함. 이런 특성은 의존성 관리 시스템을 혼란스럽게 만듦.

Plug'n'Play( pnp, zero install )

Yarn Berry는 위에서 언급한 문제를 Plug’n’Play 전략 이용해 해결.
yarn berry는 node_modules를 사용하지 않음. 대신 .yarn 경로 하위에 의존성들을 .zip 포맷으로 압축 저장하고, .pnp.cjs 파일을 생성 후 의존성 트리 정보를 단일 파일에 저장함. 이를 인터페이스 링커 (Interface Linker) 라고 함.

링커를 논리적 종속성 트리와 파일 시스템 사이에 있는 일종의 접착제로도 비유할 수 있음. 이러한 링커를 사용함으로서 패키지를 검색하기 위한 비효율적이고 반복적인 디스크 I/O로부터 벗어날 수 있게 됨. 의존성 또한 쉽게 검증할 수 있어 유령 의존성 문제도 해결 가능해짐.

pnp 배경

Yarn v1은 package.json 파일을 기반으로 의존성 트리를 생성하고, 디스크에 node_modules 디렉토리 구조를 만듦. 이미 패키지의 의존성 구조를 완전히 알고 있는 것.

node_modules 파일 시스템을 이용한 의존성 관리는 깨지기 쉬움. 모든 패키지 매니저가 실수하기 쉬운 Node 내장 의존성 관리 시스템을 사용해야 할까요? 패키지 매니저들이 node_modules 디렉토리 구조를 만드는 것에 그치지 않고, 보다 근본적으로 안전하게 의존성을 관리하면 어떨까?

Plug’n’Play는 이런 생각에서 출발.

Plug’n’Play 켜기

NPM에서 최신 버전의 Yarn을 내려받고, 버전을 Berry로 설정하면 Yarn Berry를 사용할 수 있음.

$ npm install -g yarn
$ cd ../path/to/some-package
$ yarn set version berry

Yarn Berry는 기존 Node.js 의존성 관리 시스템과 많이 다르기 때문에 하위호환을 위해 패키지 단위로만 도입할 수 있음.

Plug’n’Play의 동작 방법

Plug’n’Play 설치 모드에서 yarn install 로 의존성을 설치했을 때, 기존과 다른 모습을 볼 수 있음.

Yarn Berry는 node_modules를 생성하지 않음. 대신 .yarn/cache 폴더에 의존성의 정보가 저장되고, .pnp.cjs 파일에 의존성을 찾을 수 있는 정보가 기록됨. .pnp.cjs를 이용하면 디스크 I/O 없이 어떤 패키지가 어떤 라이브러리에 의존하는지, 각 라이브러리는 어디에 위치하는지를 바로 알 수 있음.

예를 들어, react 패키지는 .pnp.cjs 파일에서 다음과 같이 나타남.

/* react 패키지 중에서 */
["react", [
  /* npm:17.0.1 버전은 */
  ["npm:17.0.1", {
    /* 이 위치에 있고 */
    "packageLocation": "./.yarn/cache/react-npm-17.0.1-98658812fc-a76d86ec97.zip/node_modules/react/",
    /* 이 의존성들을 참조한다. */
    "packageDependencies": [
      ["loose-envify", "npm:1.4.0"],
      ["object-assign", "npm:4.1.1"]
    ],
  }]
]],

react 17.0.1 버전 패키지의 위치와 의존성의 목록을 완전하게 기술하고 있는 것을 확인할 수 있음. 이로부터 특정 패키지와 의존성에 대한 정보가 필요할 때 바로 알 수 있음.
기존 Node 가 파일시스템에 접근하여 직접 I/O 를 실행하던 require 문 비효율을 자료구조를 메모리에 올리는 방식으로 탐색 최적화한 것. 의존성 압축을 통하여 디스크 용량 절감 효과도 볼 수 있음. du -sh 명령어로 확인해보았을 때, Next.js 기반 어드민 서비스 기준 913MB → 247MB 로 기존 패키지 용량 대비 약 27% 수준으로 패키지 관련 용량이 감소한 것을 확인할 수 있음.

Yarn은 Node.js가 제공하는 require() 문의 동작을 덮어씀으로써 효율적으로 패키지를 찾을 수 있도록 함. 이 때문에 PnP API를 이용하여 의존성 관리를 하고 있을 때에는 node 명령어 대신 yarn node 명령어를 사용해야 함.

$ yarn node

일반적으로 Node.js 앱을 실행할 때에는 package.json의 scripts 에 실행 스크립트를 등록하여 사용하게 됨. 이때 Yarn v1에서 사용하던 것처럼 Yarn으로 스크립트를 실행하기만 하면 자동으로 PnP로 의존성을 불러옴.

$ yarn dev

ZipFS (Zip Filesystem)


Yarn PnP 시스템에서 각 의존성은 Zip 아카이브로 관리됨. 예를 들어, Recoil 0.1.2 버전은 recoil-npm-0.1.2-9a0edbd2b9-c69105dd7d.zip과 같은 압축 파일로 관리.

이후 .pnp.cjs 파일이 지정하는 바에 따라 동적으로 Zip 아카이브의 내용이 참조됨. ( 즉, 패키지들간 의존성을 .pnp.js 파일에서 일괄적으로 관리해서 빠르고 명확하게 의존 관계 정리함. )

Archive
백업 또는 다른 장소로의 이동시키는 등의 목적을 위해 컴퓨터 파일들을 뭉쳐놓은 모음.

Zip 아카이브로 의존성을 관리하면 다음과 같은 장점이 있음.

  • 더 이상 node_modules 디렉토리 구조를 생성할 필요가 없기 때문에 설치가 빠름.
  • 각 패키지는 버전마다 하나의 Zip 아카이브만을 가지기 때문에 중복해서 설치되지 않음. 각 Zip 아카이브가 압축되어 있음을 고려할 때, 스토리지 용량을 크게 아낄 수 있음.
    • 실제로 의존성이 차지하는 크기를 대폭 감축할 수 있음.
    • 한 서비스의 경우 NPM을 이용했을 때 node_modules 디렉토리가 약 400MB를 차지했지만, Yarn PnP를 사용했을 때 의존성 디렉토리의 크기는 120MB에 불과함.
  • 의존성을 구성하는 파일의 수가 많지 않으므로, 변경 사항을 감지하거나 전체 의존성을 삭제하는 작업이 빠름.
    • 없는 의존성이나 더 이상 필요 없는 의존성을 쉽게 찾을 수 있음.
    • Zip 파일의 내용이 변경되었을 때에는 체크섬과 비교하여 쉽게 변경 여부를 감지할 수 있음.

체크섬
데이터에 오류가 있는지 확인하는 데 사용되는 일련의 숫자와 문자. 원본 파일의 체크섬을 알고있는 경우 체크섬 유틸리티를 사용하여 복사본이 동일한 지 확인할 수 있음. 네트워크 문제로 인해 파일이 제대로 다운로드되지 않았거나 하드 드라이브 문제로 인해 디스크의 파일이 손상되었을 수 있는데 이때 원본 파일의 체크섬을 알고있는 경우 체크섬 또는 해싱 유틸리티를 실행해서 원본과 동일한 체크섬인지 비교할 수 있음.

Plug’n’Play 도입 결과

토스 프론트엔드 챕터가 Plug’n’Play를 도입한 결과, 다양한 장점을 느낄 수 있었음.

의존성을 검색할 때

의존성을 검색할 때, 더 이상 node_modules 폴더를 순회할 필요가 없음. .pnp.cjs 파일이 제공하는 자료구조를 이용하여 바로 의존성의 위치를 찾기 때문. require()에 걸리는 시간이 크게 단축됨.

재현 가능성

패키지 모든 의존성은 .pnp.cjs 파일을 이용하여 관리되기 때문에 더 이상 외부 환경에 영향받지 않음. 이로써 다양한 기기 및 CI 환경에서 require() 또는 import 문의 동작이 동일할 것임을 보장할 수 있게 됨.

의존성을 설치할 때

더 이상 설치를 위해 깊은 node_modules 디렉토리를 생성하지 않아도 됨. 또 NPM이 설치하는 것처럼 같은 버전의 패키지가 여러 번 복사되어 설치 시간을 극단적으로 단축할 수 있음. 이에 더해 Zero-install을 사용하면 대부분 라이브러리를 설치 없이 사용할 수 있음.

이를 이용하면 CI와 같이 반복적으로 의존성 설치 작업이 이루어지는 곳에서 시간을 크게 절약할 수 있음. 토스팀에서는 원래 CI에서 60초씩 걸리던 설치 작업을 Yarn PnP를 도입함으로써 수 초 이내로 단축.

엄격한 의존성 관리

Yarn PnP는 node_modules에서와 같이 의존성을 끌어올리지 않음. 이로써 각 패키지들은 자신이 package.json에 기술하는 의존성에만 접근할 수 있음. 기존에 환경에 따라 우연히 작동할 수 있었던 코드들이 보다 엄격히 관리되는 것. 이로써 예기치 못한 버그를 쉽게 일으키던 유령 의존성 현상을 근본적으로 막을 수 있었음.

MonoRepo

패키지 의도하지 않은 호이스팅을 허용하지 않는 특성이 모노레포에서도 그대로 적용.

다른 방식으로 구축한 모노레포에서는 루트에 공통으로 쓸 패키지를 선언해 설치하고 각 프로젝트에서는 특별히 사용하는 패키지만 의존성에 추가하는 방식을 사용하는데, 이 방식은 node_modules가 패키지를 찾는 방식(= 호이스팅)에 기대고 있는 방식이라 yarn berry workspace로 구현한 모노레포에는 통하지 않음. 따라서 각 패키지에서 쓸 모듈은 루트에 패키지를 추가했는가와는 상관없이 무조건 하위 프로젝트의 의존성으로도 추가해 주어야 함.

의존성 검증

node_modules를 사용하여 의존성을 관리했을 때에는 올바르게 의존성이 설치되지 못해서 의존성 폴더 전체를 지우고 다시 설치해야 하는 경우가 발생하고는 했음. node_modules 폴더를 검증하기 어려웠기 때문. 전체 재설치를 수행할 때 node_modules 디렉토리 구조를 다시 만드느라 1분 이상의 시간이 허비되기도 함.

EX)
팀오투에서는 왜 Yarn Berry를 고민하게 되었나
2022년 5월, 카모아에 최소 1개월부터 최대 12개월까지 매월 정기결제를 지원하는 렌트상품인 ‘월구독’ 서비스가 런칭되었습니다! (많관부 😝)

수개월 간 기획, 디자인, 개발, QA를 거쳐 탄생한 아주 소중한 결과물이 DEV, STAGE를 거쳐 드디어 LIVE에 배포를 한 순간..!
.
.
아무것도 뜨지 않았습니다 😩😩😩
저희는 황급히 롤백한 다음 문제를 찾기 시작했고, 개발자 도구는 번들링된 웹팩 코드의 한 꼭지를 가르키고 있었고, 특정 id에 해당하는 모듈을 찾지 못해서 발생한 것으로 보였습니다.

배포를 위해 프로덕션 빌드를 돌린 장비에서만 해당 현상이 발생했고, node_modules를 지우고 다시 npm install 을 실행하고 다시 빌드하니 멀쩡하게 잘 돌아갔고 찜찜하지만 그렇게 배포를 하게 되었습니다.

이후 그 에러를 재현하려고 수차례 시도해봤지만 너무나 잘 돌아가는 탓에, 여전히 정확한 원인을 이해하지 못했습니다. 하지만 이 일로 인해 한 가지 확실하게 알게 된 것은 모든 개발자의 node_modules가 각기 다를 수 있다는 것이었습니다.

문제의 재발을 막기 위해서는 패키지 관리를 더욱 엄격하게 할 방법을 찾기 시작했고,
Yarn PnP에서는 Zip 파일을 이용하여 패키지를 관리하기 때문에 빠진 의존성을 찾거나 의존성 파일이 변경되었음을 찾기 쉬움. 이로써 의존성이 잘못되었을 때 쉽게 바로잡을 수 있음. 이로써 올바르게 의존성이 설치되는 것을 100%에 가깝게 보장할 수 있음.
1. 패키지 버저닝을 ^x.x.x까지만 허용.
(minor 업데이트까지만 허용)
기존package.json 파일을 보니 latest, ^x.x.x , ~x.x.x 등 명확한 기준 없이 패키지 버전 관리가 이루어지고 있었고, 따라서 오래 전에 npm install을 실행한 개발자와 최근에 온보딩한 개발자의 node_modules는 당연히 다를 수 밖에 없게 됨.

일단 들쑥날쑥한 의존성 버저닝을 ^x.x.x 형태로 통일하여 최소한 major 버전까지 바뀌는 일은 없도록 대응

이 정도면 충분하지 않냐고 할 수도 있지만, 그렇지 않음. 시맨틱 버저닝 원칙에 따라 minor 버전 간 호환성이 지켜지는지는 외부 패키지 개발자의 의지에 달려있고, 여전히 개발자들은 서로 다른 node_modules를 가지고 있기 때문.
2. Yarn Berry를 도입.
(optional, but recommended)
각자 네트워크에서 패키지를 다운로드 받는 게 아니라 동일한 파일을 source로 사용하게 때문.

zero-Install

Yarn Berry의 PnP를 도입함으로써 얻을 수 있는 다양한 장점들을 살펴봄. 그럼 의존성도 Git 등을 이용하여 버전 관리를 하면 어떨까?

Yarn PnP은 의존성을 압축 파일로 관리하기 때문에 용량이 작음. 또한 각 의존성은 하나의 Zip 파일로만 표현되기 때문에 의존성을 구성하는 파일의 숫자가 NPM만큼 많지 않음. 예를 들어, 일반적인 node_modules 는 1.2GB 크기이고 13만 5천개의 파일로 구성되어 있는 반면, Yarn PnP의 의존성은 139MB 크기의 2천개 압축 파일로 구성됨. 용량도 많이 줄어듦.

이처럼 용량과 파일의 숫자가 적기 때문에 Yarn Berry를 사용하면 의존성을 Git으로 관리할 수 있음. 그리고 이렇게 의존성의 버전을 관리할 때 더욱 큰 장점들을 발견할 수 있음.

이렇게 Yarn Berry에서 의존성을 버전 관리에 포함하는 것을 Zero-Install이라고 함.

  • 의존성을 버전 관리에 포함하면 많은 장점들이 생김.
    커밋에 포함시켜 github에 프로젝트 코드와 함께 올려두면 어디서든 같은 환경에서 실행 가능할 것을 보장 가능.
    새로 저장소를 복제하거나 브랜치를 바꾸었다고 해서 yarn install을 실행하지 않아도 됨. ( 별도 설치 과정 필요 없음. )
    일반적으로 다른 의존성을 사용하는 곳으로 브랜치를 변경했을 때, 잊지 않고 의존성을 설치 해주어야 했음. 경우에 따라서는 잘못된 의존성 버전이 사용됨으로써 웹 서비스가 알 수 없는 이유로 오동작하기도 했음. Zero-Install을 사용했을 때 이런 문제는 완전히 해결됨.
    ( 패키지 A를 B로 마이그레이션한 브랜치가 있다고 가정해봅시다. node_modules 방식에서는 개발자가 브랜치를 switch할 때마다 install을 해주지 않으면 node_modules에서 패키지를 찾지 못하고 에러를 보냄.
    PnP 방식에서는 패키지 zip 파일 자체를 버전관리할 수 있게 되어 각 커밋에 맞는 의존성 컨텍스트가 생김.
    브랜치를 checkout할 때마다 .yarn/cache 폴더에 있는 의존성도 커밋으로 잡혀있기 때문에 여타 파일들처럼 파일로 취급되어 함께 변경됨. 어떤 브랜치, 커밋으로 돌아간다고 해도 .yarn/cache 폴더에 즉시 사용 가능한 패키지들이 기다리고 있음. )

  • 네트워크가 끊어진 곳에서는 오프라인 캐시 기능을 해주기도 함.

  • 의존성에 변경이 발생 해도 git상에서 diff로 잡히므로 쉽게 파악 가능. 개발자들 간 node_mdules가 동일한지 체크 할 필요 없음. Yarn berry 도입 시 가장 강조 되어야 할 중요한 지점이라고 생각. 우리가 작성한 코드들이 여러 툴체인을 거치는 동안 많은 파일들이 generate 되는데, 만약 로컬에 설치된 파일과 리모트(CI 환경, 실서비스 등)에 설치된 파일이 달라 디버깅을 어렵게 한다면 대응하기 매우 어려워질 것. Zero Install을 사용하게 된다면 어떤 설치 환경에서든 같은 상황임을 명시적으로 보장할 수 있음.

  • CI에서 의존성 설치하는 시간 크게 절약할 수 있음. 일반적으로 캐시가 존재하지 않을 때 의존성을 설치하기 위해서 60초~90초 시간이 필요함.
    Zero-Install을 사용하면 패키지 설치 단축을 위한 cache 공간이 필요없음. 저장소에서 checkout 받은 그대로 사용. Git Clone으로 저장소를 복제했을 때 의존성들이 바로 사용 가능한 상태가 되어, 의존성을 설치할 필요가 없음. 이로써 CI 시간을 크게 절약할 수 있음.
    (네트워크에서 패키지를 다운로드 받는 시간이 사라지므로 보통 분 단위로 과금하는 CI/CD 솔루션을 사용할 때 비용 절감 효과를 기대.)

CI
지속적인통합은 운영을 하면서 기능이나 개선을 더해가는 것.

Zero-install 기능을 적극적으로 레포지토리에 도입함으로써 빌드와 배포 시간을 크게 단축할 수 있었음.

.gitignore에 추가

# Zero-Install을 사용하겠다면?
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Zero-Install을 사용하지 않겠다면?
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

Yarn Berry 그 외 장점

플러그인 시스템

Yarn Berry는 핵심 기능도 플러그인을 이용해 개발되어 있을 만큼 플러그인 친화적인 환경을 자랑. 필요한 만큼 Yarn 기능을 확장하여 손쉽게 CLI로 사용할 수 있음.

토스 프론트엔드 챕터에서는 이현섭님께서 변경된 워크스페이스를 계산하는 플러그인을 며칠만에 만들어주시기도 하셨음. 이처럼 Yarn Berry의 기능이 부족하다면 손쉽게 플러그인을 만들 수 있음.

workspace

workspace는 monorepo 내 멀티 패키지 의존성 관리를 도와주는 툴. 중복 모듈을 한 곳에 모아 관리할 수 있고, 공유 모듈의 참조가 쉬워진다는 것이 장점.

Yarn Berry는 Yarn v1와 비교할 수 없을 정도로 높은 완성도 WorkSpace 기능 제공. Yarn Berry Git 레포지토리에서 대표적으로 사용하는 모습 확인할 수 있음. TypeScript를 사용함에도 한 패키지 소스 코드 변경사항이 즉시 다른 패키지에 반영되는 모습이 인상적.

Toss Frontend Chapter에서도 적극적으로 WorkSpace 기능 사용.

패치 명령어 기본 지원

경우에 따라서 NPM에 배포된 라이브러리 일부분만 수정해서 사용 하고 싶은 니즈가 있음. Yarn Berry는 yarn patch 명령어를 제공함으로써 쉽게 라이브러리의 일부분을 수정해서 사용할 수 있도록 함. 이렇게 만든 패치 파일은 patch: 프로토콜을 이용해서 쉽게 의존성 설치에 사용할 수 있음.

토스 팀은 이렇게 Yarn Berry를 도입함으로써 JavaScript 의존성을 효율적이고 안전하게 다룰 수 있었음. 오래 걸리던 CI 속도를 60초 이상 단축하기도 함.

MonoRepo

이용자가 늘어날수록 확장성을 고려해야 하고, 안정적으로 운영하고 손쉽게 새로운 기능을 더할 수 있도록 유지보수성도 있어야 함.
신뢰성이 높고 에러가 적은 어플리케이션을 개발도 고민해야 하고, 개발자가 늘어날수록 개발 편의성도 고려해야 함. 그리고 이 조건들을 채워가며 빠르게 성장도 해야 함.
위에 언급한 많은 조건들을 충족시키기 위한 고민은 오래 전부터 있어옴.
그 중 하나가 2000년 초 쯤에 나온 개념인 Monorepo.
Monorepo는 하나의 저장소 안에서 여러 프로젝트들을 통합하여 관리.

장점

  • 코드 재 사용
  • 외부 의존성 관리
  • 모든 프로젝트 아우르는 리팩토링
  • 공통 설정 및 모듈 단일화
    개발해 둔 공통 UI 컴포넌트 라이브러리와 같이 모듈화된 코드를 여러 프로젝트에서 재 사용할 수 있음.
  • package 공유를 통한 중복 코드 감소
  • 단일 이슈 트래킹
  • 통합 CI 및 테스트
  • 프로젝트 간 협업
  • FE Project SourceCode를 한곳에서 관리해서 소스 변경사항을 보다 쉽게 파악 가능. 신규 입사자 합류 시 개발 환경 세팅하는데 비용 줄일 수 있음.
  • 코드 정적 분석 및 테스트 도구를 공통으로 적용함으로써 코드 최소 품질을 유지할 수 있을 것이라고 생각.

단점

  • repository 거대화
  • CI build 저하
  • Package간 무분별한 의존성
  • Dev Tools 인덱싱 저하

yarn berry는 plugin 시스템이나 강화된 workspace 지원으로 lerna
( Yarn classic은 기능 지원이 적어 Lerna등을 병행해서 관리 도구로 사용 )같은 추가 라이브러리가 없어도 모노레포 구조 잘 지원.

참고
https://joshua1988.github.io/vue-camp/package-manager/npm-vs-yarn.html#npm%E1%84%80%E1%85%AA-yarn%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7

https://velog.io/@khy226/Yarn-%EC%9D%B4%EB%9E%80

https://toss.tech/article/node-modules-and-yarn-berry

https://blog.dramancompany.com/2023/02/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A2%8C%EC%B6%A9%EC%9A%B0%EB%8F%8C-yarn-berry-%EB%8F%84%EC%9E%85%EA%B8%B0/

https://www.google.com/search?q=yarn+berry&rlz=1C5CHFA_enKR1038KR1038&ei=SekFZNasJonT-QahiL24BQ&start=0&sa=N&ved=2ahUKEwiW9b6Xs8f9AhWJad4KHSFED1c4FBDy0wN6BAgDEAQ&biw=1473&bih=936&dpr=1

https://channel.io/ko/blog/frontend_yarnberry

https://velog.io/@oimne/yarn-berry

https://techblog.woowahan.com/7976/

https://medium.com/teamo2/yarn-berry-%EA%B5%B3%EC%9D%B4-%EB%8F%84%EC%9E%85%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-d6221b9beca6

profile
선명한 기억보다 흐릿한 메모

0개의 댓글