
프로젝트에서 모노레포를 구현해보면서 yarn berry workspace를 사용해 구현해야했는데요, 저는 Npm이라는 패키지 매니저만 사용해봤기 때문에, yarn은 아주잠깐써보고 yarn berry는 써본적이 없었어서 각 어떤 차이가 있고 어떤 장단점이 있는지 정리해볼까합니다.
모노레포를 구축하면서 마주했던 에러들은 따로 모아 기술할 예정입니다.
Npm
일관적이지 않은 패키지 버전
프론트엔드 개발자가 가장 먼저 접할 수 있는 패키지매니저는 npm일 것 같다는 생각이 들어요. 저도 가장 많이 접하고, 자주 사용했던게 npm이었습니다. 노드 모듈은 기본적으로 시멘틱 버저닝이라는 기법을 사용해서 사용하는 모듈들의 버전을 기입할 것을 권장하고 있다고 합니다. 시멘틱 버저닝이란 간단하게 말해서 `1.2.3` 처럼 버전을 세가지 숫자가 들어갈 수 있는 자리로 구분하고, 각 자리에 현재 버전이 이전 버전과 어떤 관계가 있는지 암시하도록 하는 방법이라고 해요. 예를 들어 첫번째 자리에 들어가는 숫자가 다르다면 두 버전은 완전히 새로운 메이저 버전이라고 생각하면 됩니다.

사진에 보면 `^(캐럿)`표시가 되어있는데, 이는 가장 앞 숫자인 메이저 숫자를 제외한 두번째, 세번째 숫자(마이너 패치)까지는 변경을 허용한다는 의미입니다. 현재 사진 속 바벨 코어 버전이 `^7.17.9` 이기때문에 메이버전인 7이 유지된 상태에서 모듈버전이 바뀐다는 것을 허용하게 됩니다. `7.18.0`이나 `7.17.0`은 모두 허용한다고 볼 수 있겠습니다.
이때 `npm install` 명령어를 통해서 모듈들을 설치하면 해당 메이저 버전 중 가장 최신버전으로 모듈을 설치하게 됩니다. 빌드에 따라서 유연하게 모듈을 설치할 수 있다는 장점이 있지만, 이럴 경우 모듈간의 버전 불일치로 충돌이 발생할 수도 있습니다.
고정되지 않은 설치 순서
개발자의 환경에 따라서 모듈들의 설치 순서가 변경된다고 합니다. 만들어진 프로젝트에서 모듈을 추가로 설치하게 될 경우, 그 뒤에 npm i명령어를 통해 처음부터 설치하는 사람과 모듈 설치 순서가 달라질 수 있다고 합니다. npm은 모듈 이름을 사전 순서대로 정렬하여 순차적으로 설치하기 때문이라고 해요. 서로 다른 환경이 만들어지기 때문에 좋은 징조라고 보기 어렵습니다.
순차적인 설치로 인한 소요시간
npm 모듈들을 한 번에 하나씩만 순차적으로 설치합니다. 설치해야하는 모듈이 많으면 많을수록, 설치 시간이 길어지게 됩니다. 첫 모듈 설치 시간 시간이 길어지면 빌드 및 배포 시간에도 부정적인 영향을 끼친다고 하네요. 아마 npm을 사용하지 않는 이유는 모듈 설치, 배포 시간이 가장 큰 이유가 아닐까 싶습니다.
이외에도 npm은 하나의 라이브러리나 모듈이 의존하고 있는 모든 모듈을 가져와 설치한다는 점이 있어서 보안적으로도 문제가 있는 것으로 알 수 있습니다. npm에서 어떤 라이브러리가 다른 라이브러리에 의존하고 있을 때, 삭제도 되지않기 때문에 여러 다양한 문제가 존재하는 것으로 알고 있습니다. 최근 코딩애플에서도 npm 관련 문제를 다룬 영상이 있어 참조합니다.
Yarn
2016년 10월에 출시되었으며 npm 문제들을 해결하기 위해서 등장하게 되었습니다.
버전 고정파일 yarn.lock 포함
yarn은 사용할 모듈의 버전을 지정하기 위해서 `.lock` 파일을 포함한다고 합니다. 정확한 버전을 지정하기 때문에 npm이 가지고 있었던 모듈 충돌문제를 마주하지 않을 수 있고, 모든 개발자들이 같은 환경에서 개발할 수 있도록 보장할 수 있게 된다고 하네요.
물론 이 시점의 npm 메인테이너들도 이러한 문제를 인지하고 있었고, 패키지의 버전을 고정시키는 방법도 제시했었습니다. `npm shrinkwrap` 명령어를 사용하면 `.lock` 파일과 유사하게 패키지들의 버전을 포함한 `npm-shrinkwrap.json`파일을 생성해줍니다.
이 파일이 프로젝트에 함께 있을경우, `package.json` 에 명시된 버전을 무시하고 `json` 파일에 적힌 버전을 설치하게 됩니다.
하지만 npm의 경우 고정이 필요할때마다 매번 명령어를 입력하여 json 파일을 생성하는 과정이 필요했습니다. yarn은 이러한 버전 고정 파일을 자동으로 생성합니다.
설치 확인을 위한 checksum 사용
yarn은 패키지가 제대로 설치되었는지 확인하기 위해 checksum을 사용한다고 해요. yarn.lock 파일을 확인해보면 resolved 주소 뒤에 해시값이 추가 되어있는 것을 확인할 수 있습니다. 이 해시값이 바로 checksum입니다.
물론 중간에 설치 요청을 가로채거나 원본을 수정하는등 공격자가 비집고 들어 올 확률은 낮지만, 제대로 설치되지 않은 경우를 대비해 패키지 파일의 무결성을 확인하는 안전장치가 추가된 것은 긍정적인 일입니다.
속도
yarn의 장점에서 빠질 수 없는 요소는 역시 속도라고 할 수 있을 것 같아요. yarn은 캐시를 사용하여 한 번 다운로드 한 패키지라면 그 다음부터는 빠른 속도로 설치할 수 있다고 해요. 인터넷이 연결되지 않은 환경에서도 설치가 가능합니다. 또한 병렬 다운로드를 지원하여 순차적으로 설치해야하는 npm과 달리 모듈들을 한꺼번에 설치해버립니다. 덕분에 npm보다 속도가 빠릅니다.
npm과 Yarn의 현재
시간이 지나고, npm도 버전이 올라가면서 많은 부분이 개선되었습니다. 현재는 버전 고정을 위한 `package-lock` 파일도 자동으로 추가되고, 속도면에서도 yarn과 큰 차이가 없는 수준까지 따라왔습니다. `node_modules` 또한 이제 결정적(deterministic)인 트리 구조로 만들 수 있게 되었다고 해요.
yarn 또한 시간이 지나며 많이 발전하였고, 사용자가 많아지고 기능들이 추가되며 yarn 생태계 자체도 차츰 안정되어 갔습니다.
그러나 여러 발전을 거친 두 패키지 매니저 프로그램들에게도 여전히 남아있는 문제점들이 있었는데요.
npm과 yarn의 문제점
🤮 npm의 비효율적인 의존성 검색
npm은 파일시스템을 이용하여 의존성을 관리합니다. 익숙한 `node_modules` 폴더를 이용하는 것이 특징입니다. 이렇게 관리했을 때, 의존성 검색은 매우 비효율적이라고 하는데요,
예를 들어서, `/Users/toss/dev/toss-frontend-libraries`폴더에서 `require()`문을 통해서 `react`패키지를 불러오는 상황이라고 가정해보겠습니다. 예시는 토스 아티클을 참조하여 가져오게 되었습니다.
라이브러리를 찾기 위해 순회하는 디렉토리 목록을 확인하려고 할때, `Node.js`에서 제공하는 `require.resolve.paths()`함수를 사용할 수 있다고 해요. 그리고 이 함수는 검색하는 디렉토리 목록을 반환합니다.

사진에서도 알 수 있듯이 npm은 패키지를 찾기 위해서 상위 디렉토리의 `node_modules` 폴더를 탐색하게 됩니다. 계속 폴더를 열어보고 닫고 하는 과정이 보기만해도 매우 비효율적이라고 느껴집니다. 패키지를 찾지 못할 수록 느린 탐색을 하게 된다고 하네요 (디스크 I/O ㅎ호출을 뜻하는 것 같습니다. 잘은 모르지만)
🤮 npm의 환경에 따라 달라지는 동작
npm 패키지를 찾지 못한다면 상위 디렉토리에서 `node_modules`를 찾게 된다고 했는데요. 예를 들어, 상위 디렉토리가 어떤 `node_modules`를 포함하고 있는지 여부에 따라서 의존성을 불러올 수 있기도하고 없기도 해버리는 의아한 상황이 일어나게 됩니다. 다른 버전의 의존성을 가져오게되는 여지도 존재한다고 하네요. 이제 보니 npm 좀 별로네요.
🤮 npm과 yarn의 유령의존성

수 많은 패키지들과 그 패키지가 의존하는 모듈들을 전부 설치해버린다면 무거운 node_modules 폴더가 생겨버리게 됩니다. npm은 이 무거운 폴더를 경량화시키기 위해 호이스팅을 도입하고 있습니다. npm은 속도 문제를 개선하기 위해 호이스팅 등 최적화 알고리즘을 도입하였으나 부작용으로 유령 의존성이라는 문제를 새로 낳고 말았습니다.

용량을 줄이는 가장 간단한 방법은 중복을 제거하는 것이죠. npm과 yarn은 `node_modules` 내부의 중복된 패키지를 최소화하기 위해 각 패키지가 의존하고 있는 패키지들을 최상단으로 끌어올려 버립니다. 이렇게 되면 프로젝트가 의존하고 있는 패키지의 내부에 존재하던 각각의 `node_modules`에 있는 중복된 패키지가 최상단에 하나만 존재하게 되어 불필요한 중복을 제거할 수 있게 됩니다.
예를 들어서, 의존성 트리가 왼쪽 사진과 같은 모습을 하고 있다고 가정했을 때, `A(1.0)`과 `B(1.0)` 패키지는 두 번 설치되게 되므로 디스크 공간을 낭비하게 됩니다. 따라서 오른쪽과 같은 모양으로 트리를 변형하게 되는데요, 오른쪽 트리로 의존성이 변형되면서 `package-1`은 원래 `require()`을 할 수 없었던 `B(1.0)`패키지를 require할 수 있게 됩니다.
이렇게 끌어올리기에 따라서 직접 의존하지 않고 있는 라이브러리를 require할 수 있는 것을 유령의존성이라고 합니다. 유령 의존성이 발생하게 되면 `package.json`에 명시되지 않은 라이브러리를 조용히 사용할 수 있게 됩니다. 다른 의존성을 `package.json`에서 삭제 하였을 때, 소리없이 사라지기도 한다고 하네요.
따라서 yarn berry에서는 이런 식의 호이스팅 동작이 일어나지 않도록 nohoist 옵션이 기본적으로 활성화 되어 있습니다.
Yarn Berry
2020년 1월 25일에 출시되었으며 `yarn v2` 이상의 `modern version yarn`을 이르는 명칭입니다.
기존의 yarn v1은 `yarn classic`이라고 부르게 되었습니다.
npm에서 최선버전의 yarn을 받고, 버전을 berry로 설정하면 yarn berry를 사용할 수 있습니다.
npm i -g yarn
cd ../path/to/some-package
yarn set version berry
yarn berry는 node.js의 의존성 관리 시스템과는 많이 다르기때문에 하위호환을 위해서 패키지 단위로만 도입할 수 있다고 해요.
Plug'n'Play (PnP)
npm에서 패키지 관리 시스템은 파일 시스템이었다면, Plug'n'Play는 yarn berry가 제공하는 새로운 패키지 관리 시스템입니다. 기존에 무거웠던 `node_modules`를 없애자는 취지가 강했던 것 같아요.

그래서 대신 `zip`파일로 압축하여 `.yarn/cache` 폴더에 저장하고 이를 찾기 위한 `.pnp.cjs`파일에 생성 후 의존성 트리 정보를 단일 파일에 저장합니다. 이를 `인터페이스 링커 (Interface Linker)`라고 합니다.
예를 들어, `react` 패키지는 `pnp.cjs` 파일에서 다음과 같이 나타납니다.

리액트 17.0.1 버전 패키지의 위치와 의존성 목록이 모두 적혀있는 것을 확인할 수 있습니다. 이로부터 특정 패키지와 의존성에 대한 정보를 바로 알 수 있습니다.
yarn은 node.js가 제공하는 `require()`문의 동작을 덮어씀으로써 효율적으로 패키지를 찾을 수 있게 됩니다. 이 때문에 PnP API로 의존성을 관리하고 있을경우에는 node 명령어 대신에 `yarn node` 명령어를 사용해야한다고 하네요.
$ yarn node
일반적으로 Node.js 앱을 실행할 때에는 `package.json`의 `scripts`에 실행 스크립트를 등록하여 사용하게 됩니다. 이때, Yarn v1에서 사용했던 것처럼 Yarn으로 스크립트 실행하기만 하면 자동으로 PnP로 의존성을 불러오게 됩니다.
$ yarn dev
그런데, 만약 `.yarnrc.yml` 링커 설정을 pnp가 아니라 `node_modules`로 하게 된다면 기존 `node_modules`를 설치하여 의존성을 관리하게 된다고 하네요. 따라서 링커 설정을 잘하는게 중요할 것 같습니다.
ZipFS(Zip FileSystem)
아래 이미지는 zip으로 묶인 라이브러리가 저장된 `.yarn/cache`폴더 입니다.

Yarn PnP 시스템에서 각 의존성은 Zip 아카이브로 관리됩니다. Zip으로 의존성을 관리하면 아래와 같은 장점이 존재합니다.
- 더이상 node_modules 디렉터리 구조를 생성할 필요가 없기 때문에 설치가 신속히 완료됩니다.
- 각 패키지는 버전마다 하나의 Zip 아카이브를 갖고 있기 때문에 중복해서 설치되지 않습니다. 또한 스토리지 용량을 크게 아낄 수 있습니다.
- 의존성을 구성하는 파일의 수가 많지 않으므로, 변경사항을 감지하거나 전체 의존성을 삭제하는 작업이 빠릅니다.
Zero-install
가장 큰 장점이 속도도 그렇지만 zero-install이지 않을까 싶습니다. Yarn Berry에서 의존성을 버전관리에 포함하는 것을 zero-install이라고 합니다.

사진은 yarn berry 깃 레포에서 사용하는 zero-install의 예시입니다.
의존성을 버전 관리에 포함하면 많은 장점들이 생긴다고 하네요.
- 새로 저장소를 복제하거나 브렌치를 바꾸었다고해서 yarn install을 할 필요가 없어집니다. 일반적으로 다른 의존성을 사용하곳으로 브렌치를 옮겨주었을 때, 잊지 않고 의존성을 설치해주어야 했는데요. 경우에 따라서 잘못된 의존성 버전이 사용됨으로써 알 수 없는 오작동이 발생하기도 했습니다. 하지만 zero-install 방식을 통해서 완전히 해결됩니다.
- CI에서 의존성을 설치하는 시간을 크게 절약할 수 있습니다.
Yarn Berry의 단점?
이외에도 장점은 많아 보이지만, 아무래도 많은 패키지들이 npm에서만 지원되는 경우가 있기 때문에 여전히 PnP를 지원하지 않는 패키지가 존재한다는 점이 있습니다. 프로젝트에 이런 패키지가 하나라도 존재한다면 PnP 방식이라 하더라도 node_modules가 따라오게 되어서, 앞서 언급한 장점들을 극한으로 누릴 수는 없게 된다고 하네요.
최근에 yarn berry workspace를 사용해서 레포 구축을 해보았는데, prettier문제가 있더라구요. 아무래도 이 문제는 예전에도 계속 있던 문제인듯 싶었습니다. 추후 yarn berry를 사용하면서 발견했던 문제들에 대해서도 기술해볼까합니다.
Reference Doc
node_modules로부터 우리를 구원해 줄 Yarn Berry
토스 프론트엔드 레포지토리 대부분에서 사용하고 있는 패키지 매니저 Yarn Berry. 채택하게 된 배경과 사용하면서 좋았던 점을 공유합니다.
toss.tech
npm vs yarn vs yarn berry
프로젝트에 yarn berry를 사용하게 되면서 npm 과 yarn, yarn berry의 차이에 대해 알아보게되었습니다.
velog.io

프로젝트에서 모노레포를 구현해보면서 yarn berry workspace를 사용해 구현해야했는데요, 저는 Npm이라는 패키지 매니저만 사용해봤기 때문에, yarn은 아주잠깐써보고 yarn berry는 써본적이 없었어서 각 어떤 차이가 있고 어떤 장단점이 있는지 정리해볼까합니다.
모노레포를 구축하면서 마주했던 에러들은 따로 모아 기술할 예정입니다.
Npm
일관적이지 않은 패키지 버전
프론트엔드 개발자가 가장 먼저 접할 수 있는 패키지매니저는 npm일 것 같다는 생각이 들어요. 저도 가장 많이 접하고, 자주 사용했던게 npm이었습니다. 노드 모듈은 기본적으로 시멘틱 버저닝이라는 기법을 사용해서 사용하는 모듈들의 버전을 기입할 것을 권장하고 있다고 합니다. 시멘틱 버저닝이란 간단하게 말해서 `1.2.3` 처럼 버전을 세가지 숫자가 들어갈 수 있는 자리로 구분하고, 각 자리에 현재 버전이 이전 버전과 어떤 관계가 있는지 암시하도록 하는 방법이라고 해요. 예를 들어 첫번째 자리에 들어가는 숫자가 다르다면 두 버전은 완전히 새로운 메이저 버전이라고 생각하면 됩니다.

사진에 보면 `^(캐럿)`표시가 되어있는데, 이는 가장 앞 숫자인 메이저 숫자를 제외한 두번째, 세번째 숫자(마이너 패치)까지는 변경을 허용한다는 의미입니다. 현재 사진 속 바벨 코어 버전이 `^7.17.9` 이기때문에 메이버전인 7이 유지된 상태에서 모듈버전이 바뀐다는 것을 허용하게 됩니다. `7.18.0`이나 `7.17.0`은 모두 허용한다고 볼 수 있겠습니다.
이때 `npm install` 명령어를 통해서 모듈들을 설치하면 해당 메이저 버전 중 가장 최신버전으로 모듈을 설치하게 됩니다. 빌드에 따라서 유연하게 모듈을 설치할 수 있다는 장점이 있지만, 이럴 경우 모듈간의 버전 불일치로 충돌이 발생할 수도 있습니다.
고정되지 않은 설치 순서
개발자의 환경에 따라서 모듈들의 설치 순서가 변경된다고 합니다. 만들어진 프로젝트에서 모듈을 추가로 설치하게 될 경우, 그 뒤에 npm i명령어를 통해 처음부터 설치하는 사람과 모듈 설치 순서가 달라질 수 있다고 합니다. npm은 모듈 이름을 사전 순서대로 정렬하여 순차적으로 설치하기 때문이라고 해요. 서로 다른 환경이 만들어지기 때문에 좋은 징조라고 보기 어렵습니다.
순차적인 설치로 인한 소요시간
npm 모듈들을 한 번에 하나씩만 순차적으로 설치합니다. 설치해야하는 모듈이 많으면 많을수록, 설치 시간이 길어지게 됩니다. 첫 모듈 설치 시간 시간이 길어지면 빌드 및 배포 시간에도 부정적인 영향을 끼친다고 하네요. 아마 npm을 사용하지 않는 이유는 모듈 설치, 배포 시간이 가장 큰 이유가 아닐까 싶습니다.
이외에도 npm은 하나의 라이브러리나 모듈이 의존하고 있는 모든 모듈을 가져와 설치한다는 점이 있어서 보안적으로도 문제가 있는 것으로 알 수 있습니다. npm에서 어떤 라이브러리가 다른 라이브러리에 의존하고 있을 때, 삭제도 되지않기 때문에 여러 다양한 문제가 존재하는 것으로 알고 있습니다. 최근 코딩애플에서도 npm 관련 문제를 다룬 영상이 있어 참조합니다.
Yarn
2016년 10월에 출시되었으며 npm 문제들을 해결하기 위해서 등장하게 되었습니다.
버전 고정파일 yarn.lock 포함
yarn은 사용할 모듈의 버전을 지정하기 위해서 `.lock` 파일을 포함한다고 합니다. 정확한 버전을 지정하기 때문에 npm이 가지고 있었던 모듈 충돌문제를 마주하지 않을 수 있고, 모든 개발자들이 같은 환경에서 개발할 수 있도록 보장할 수 있게 된다고 하네요.
물론 이 시점의 npm 메인테이너들도 이러한 문제를 인지하고 있었고, 패키지의 버전을 고정시키는 방법도 제시했었습니다. `npm shrinkwrap` 명령어를 사용하면 `.lock` 파일과 유사하게 패키지들의 버전을 포함한 `npm-shrinkwrap.json`파일을 생성해줍니다.
이 파일이 프로젝트에 함께 있을경우, `package.json` 에 명시된 버전을 무시하고 `json` 파일에 적힌 버전을 설치하게 됩니다.
하지만 npm의 경우 고정이 필요할때마다 매번 명령어를 입력하여 json 파일을 생성하는 과정이 필요했습니다. yarn은 이러한 버전 고정 파일을 자동으로 생성합니다.
설치 확인을 위한 checksum 사용
yarn은 패키지가 제대로 설치되었는지 확인하기 위해 checksum을 사용한다고 해요. yarn.lock 파일을 확인해보면 resolved 주소 뒤에 해시값이 추가 되어있는 것을 확인할 수 있습니다. 이 해시값이 바로 checksum입니다.
물론 중간에 설치 요청을 가로채거나 원본을 수정하는등 공격자가 비집고 들어 올 확률은 낮지만, 제대로 설치되지 않은 경우를 대비해 패키지 파일의 무결성을 확인하는 안전장치가 추가된 것은 긍정적인 일입니다.
속도
yarn의 장점에서 빠질 수 없는 요소는 역시 속도라고 할 수 있을 것 같아요. yarn은 캐시를 사용하여 한 번 다운로드 한 패키지라면 그 다음부터는 빠른 속도로 설치할 수 있다고 해요. 인터넷이 연결되지 않은 환경에서도 설치가 가능합니다. 또한 병렬 다운로드를 지원하여 순차적으로 설치해야하는 npm과 달리 모듈들을 한꺼번에 설치해버립니다. 덕분에 npm보다 속도가 빠릅니다.
npm과 Yarn의 현재
시간이 지나고, npm도 버전이 올라가면서 많은 부분이 개선되었습니다. 현재는 버전 고정을 위한 `package-lock` 파일도 자동으로 추가되고, 속도면에서도 yarn과 큰 차이가 없는 수준까지 따라왔습니다. `node_modules` 또한 이제 결정적(deterministic)인 트리 구조로 만들 수 있게 되었다고 해요.
yarn 또한 시간이 지나며 많이 발전하였고, 사용자가 많아지고 기능들이 추가되며 yarn 생태계 자체도 차츰 안정되어 갔습니다.
그러나 여러 발전을 거친 두 패키지 매니저 프로그램들에게도 여전히 남아있는 문제점들이 있었는데요.
npm과 yarn의 문제점
🤮 npm의 비효율적인 의존성 검색
npm은 파일시스템을 이용하여 의존성을 관리합니다. 익숙한 `node_modules` 폴더를 이용하는 것이 특징입니다. 이렇게 관리했을 때, 의존성 검색은 매우 비효율적이라고 하는데요,
예를 들어서, `/Users/toss/dev/toss-frontend-libraries`폴더에서 `require()`문을 통해서 `react`패키지를 불러오는 상황이라고 가정해보겠습니다. 예시는 토스 아티클을 참조하여 가져오게 되었습니다.
라이브러리를 찾기 위해 순회하는 디렉토리 목록을 확인하려고 할때, `Node.js`에서 제공하는 `require.resolve.paths()`함수를 사용할 수 있다고 해요. 그리고 이 함수는 검색하는 디렉토리 목록을 반환합니다.

사진에서도 알 수 있듯이 npm은 패키지를 찾기 위해서 상위 디렉토리의 `node_modules` 폴더를 탐색하게 됩니다. 계속 폴더를 열어보고 닫고 하는 과정이 보기만해도 매우 비효율적이라고 느껴집니다. 패키지를 찾지 못할 수록 느린 탐색을 하게 된다고 하네요 (디스크 I/O ㅎ호출을 뜻하는 것 같습니다. 잘은 모르지만)
🤮 npm의 환경에 따라 달라지는 동작
npm 패키지를 찾지 못한다면 상위 디렉토리에서 `node_modules`를 찾게 된다고 했는데요. 예를 들어, 상위 디렉토리가 어떤 `node_modules`를 포함하고 있는지 여부에 따라서 의존성을 불러올 수 있기도하고 없기도 해버리는 의아한 상황이 일어나게 됩니다. 다른 버전의 의존성을 가져오게되는 여지도 존재한다고 하네요. 이제 보니 npm 좀 별로네요.
🤮 npm과 yarn의 유령의존성

수 많은 패키지들과 그 패키지가 의존하는 모듈들을 전부 설치해버린다면 무거운 node_modules 폴더가 생겨버리게 됩니다. npm은 이 무거운 폴더를 경량화시키기 위해 호이스팅을 도입하고 있습니다. npm은 속도 문제를 개선하기 위해 호이스팅 등 최적화 알고리즘을 도입하였으나 부작용으로 유령 의존성이라는 문제를 새로 낳고 말았습니다.

용량을 줄이는 가장 간단한 방법은 중복을 제거하는 것이죠. npm과 yarn은 `node_modules` 내부의 중복된 패키지를 최소화하기 위해 각 패키지가 의존하고 있는 패키지들을 최상단으로 끌어올려 버립니다. 이렇게 되면 프로젝트가 의존하고 있는 패키지의 내부에 존재하던 각각의 `node_modules`에 있는 중복된 패키지가 최상단에 하나만 존재하게 되어 불필요한 중복을 제거할 수 있게 됩니다.
예를 들어서, 의존성 트리가 왼쪽 사진과 같은 모습을 하고 있다고 가정했을 때, `A(1.0)`과 `B(1.0)` 패키지는 두 번 설치되게 되므로 디스크 공간을 낭비하게 됩니다. 따라서 오른쪽과 같은 모양으로 트리를 변형하게 되는데요, 오른쪽 트리로 의존성이 변형되면서 `package-1`은 원래 `require()`을 할 수 없었던 `B(1.0)`패키지를 require할 수 있게 됩니다.
이렇게 끌어올리기에 따라서 직접 의존하지 않고 있는 라이브러리를 require할 수 있는 것을 유령의존성이라고 합니다. 유령 의존성이 발생하게 되면 `package.json`에 명시되지 않은 라이브러리를 조용히 사용할 수 있게 됩니다. 다른 의존성을 `package.json`에서 삭제 하였을 때, 소리없이 사라지기도 한다고 하네요.
따라서 yarn berry에서는 이런 식의 호이스팅 동작이 일어나지 않도록 nohoist 옵션이 기본적으로 활성화 되어 있습니다.
Yarn Berry
2020년 1월 25일에 출시되었으며 `yarn v2` 이상의 `modern version yarn`을 이르는 명칭입니다.
기존의 yarn v1은 `yarn classic`이라고 부르게 되었습니다.
npm에서 최선버전의 yarn을 받고, 버전을 berry로 설정하면 yarn berry를 사용할 수 있습니다.
npm i -g yarn
cd ../path/to/some-package
yarn set version berry
yarn berry는 node.js의 의존성 관리 시스템과는 많이 다르기때문에 하위호환을 위해서 패키지 단위로만 도입할 수 있다고 해요.
Plug'n'Play (PnP)
npm에서 패키지 관리 시스템은 파일 시스템이었다면, Plug'n'Play는 yarn berry가 제공하는 새로운 패키지 관리 시스템입니다. 기존에 무거웠던 `node_modules`를 없애자는 취지가 강했던 것 같아요.

그래서 대신 `zip`파일로 압축하여 `.yarn/cache` 폴더에 저장하고 이를 찾기 위한 `.pnp.cjs`파일에 생성 후 의존성 트리 정보를 단일 파일에 저장합니다. 이를 `인터페이스 링커 (Interface Linker)`라고 합니다.
예를 들어, `react` 패키지는 `pnp.cjs` 파일에서 다음과 같이 나타납니다.

리액트 17.0.1 버전 패키지의 위치와 의존성 목록이 모두 적혀있는 것을 확인할 수 있습니다. 이로부터 특정 패키지와 의존성에 대한 정보를 바로 알 수 있습니다.
yarn은 node.js가 제공하는 `require()`문의 동작을 덮어씀으로써 효율적으로 패키지를 찾을 수 있게 됩니다. 이 때문에 PnP API로 의존성을 관리하고 있을경우에는 node 명령어 대신에 `yarn node` 명령어를 사용해야한다고 하네요.
$ yarn node
일반적으로 Node.js 앱을 실행할 때에는 `package.json`의 `scripts`에 실행 스크립트를 등록하여 사용하게 됩니다. 이때, Yarn v1에서 사용했던 것처럼 Yarn으로 스크립트 실행하기만 하면 자동으로 PnP로 의존성을 불러오게 됩니다.
$ yarn dev
그런데, 만약 `.yarnrc.yml` 링커 설정을 pnp가 아니라 `node_modules`로 하게 된다면 기존 `node_modules`를 설치하여 의존성을 관리하게 된다고 하네요. 따라서 링커 설정을 잘하는게 중요할 것 같습니다.
ZipFS(Zip FileSystem)
아래 이미지는 zip으로 묶인 라이브러리가 저장된 `.yarn/cache`폴더 입니다.

Yarn PnP 시스템에서 각 의존성은 Zip 아카이브로 관리됩니다. Zip으로 의존성을 관리하면 아래와 같은 장점이 존재합니다.
- 더이상 node_modules 디렉터리 구조를 생성할 필요가 없기 때문에 설치가 신속히 완료됩니다.
- 각 패키지는 버전마다 하나의 Zip 아카이브를 갖고 있기 때문에 중복해서 설치되지 않습니다. 또한 스토리지 용량을 크게 아낄 수 있습니다.
- 의존성을 구성하는 파일의 수가 많지 않으므로, 변경사항을 감지하거나 전체 의존성을 삭제하는 작업이 빠릅니다.
Zero-install
가장 큰 장점이 속도도 그렇지만 zero-install이지 않을까 싶습니다. Yarn Berry에서 의존성을 버전관리에 포함하는 것을 zero-install이라고 합니다.

사진은 yarn berry 깃 레포에서 사용하는 zero-install의 예시입니다.
의존성을 버전 관리에 포함하면 많은 장점들이 생긴다고 하네요.
- 새로 저장소를 복제하거나 브렌치를 바꾸었다고해서 yarn install을 할 필요가 없어집니다. 일반적으로 다른 의존성을 사용하곳으로 브렌치를 옮겨주었을 때, 잊지 않고 의존성을 설치해주어야 했는데요. 경우에 따라서 잘못된 의존성 버전이 사용됨으로써 알 수 없는 오작동이 발생하기도 했습니다. 하지만 zero-install 방식을 통해서 완전히 해결됩니다.
- CI에서 의존성을 설치하는 시간을 크게 절약할 수 있습니다.
Yarn Berry의 단점?
이외에도 장점은 많아 보이지만, 아무래도 많은 패키지들이 npm에서만 지원되는 경우가 있기 때문에 여전히 PnP를 지원하지 않는 패키지가 존재한다는 점이 있습니다. 프로젝트에 이런 패키지가 하나라도 존재한다면 PnP 방식이라 하더라도 node_modules가 따라오게 되어서, 앞서 언급한 장점들을 극한으로 누릴 수는 없게 된다고 하네요.
최근에 yarn berry workspace를 사용해서 레포 구축을 해보았는데, prettier문제가 있더라구요. 아무래도 이 문제는 예전에도 계속 있던 문제인듯 싶었습니다. 추후 yarn berry를 사용하면서 발견했던 문제들에 대해서도 기술해볼까합니다.
Reference Doc
node_modules로부터 우리를 구원해 줄 Yarn Berry
토스 프론트엔드 레포지토리 대부분에서 사용하고 있는 패키지 매니저 Yarn Berry. 채택하게 된 배경과 사용하면서 좋았던 점을 공유합니다.
toss.tech
npm vs yarn vs yarn berry
프로젝트에 yarn berry를 사용하게 되면서 npm 과 yarn, yarn berry의 차이에 대해 알아보게되었습니다.
velog.io