그룹 프로젝트를 시작하면서 팀원들과 함께 패키지 매니저 선택에 대한 고민을 하게 되었다. 기존에 많이 사용되어 온 npm의 문제점을 개선한 다양한 모던 패키지 매니저가 등장했는데, 그 중 특히 주목받고 있는 pnpm 을 사용하기로 했다. 멘토님께서 yarn berry도 언급해 주셔서 pnpm이 yarn berry와 비교를 해본 뒤 패키지 매니저를 결정하기로 했다. (근데 사실 이미 pnpm을 사용하기로 이야기를 나눈 후에 yarn berry도 찾아보는 형식으로 이야기가 진행된 것이라 pnpm 걍 쓸 것 같다.)
이 글에서 npm의 문제점을 짚어보고, 이를 해결하려는 yarn berry와 pnpm에 대해 간략하게 정리했다.
기존 npm(또는 Yarn 1)의 문제점
유령 의존성 (Phantom Dependency)
https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
기존 npm은 각 프로젝트마다 의존성 패키지를 node_modules 폴더에 설치한다. 이 방식은 간단하지만, 의존성 패키지가 중복 설치되는 문제가 생길 수 있다.
예를 들어 이미지의 왼쪽처럼 의존성 트리가 만들어진다고 가정하자. 여러 패키지가 A와 B에 의존할 때, 각각의 의존성에 따라node_modules 폴더에 중복으로 설치되어 디스크를 낭비하게 된다. 이를 보완하고자 npm(과 Yarn 1)은 이미지의 오른쪽처럼 중복되는 패키지를 끌어올리는(Hoisting) 방법을 사용했다.
예시로 express 패키지를 살펴보면 package.json에 다음과 같은 의존성 패키지가 명시되어 있다.
"dependencies": {
**"accepts": "~1.3.8",**
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
**"debug": "2.6.9",**
...
},
하지만, 이렇게 끌어올리기를 하게 되면 직접 의존하지 않던 B 라이브러리를 불러올 수 있게 되는 문제점이 발생한다. 이를 유령 의존성이라고 부른다. 즉, package.json에 명시하지 않은 라이브러리를 몰래 사용하게 되면서 의존성 관리에 혼란을 야기할 수 있는 것이다.
이를 해결하는 Yarn Berry와 Pnpm
Yarn Berry와 Pnpm 모두 호이스팅 대신 각자의 방식을 채택해 유령 의존성 문제를 해결했다.
Yarn Berry의 해결 방법
Yarn Berry(Yarn 2)는 이러한 문제를 해결하기 위해 Plug'n'Play(PnP) 방식을 도입했다. (이름이 너무 복잡한데…) Yarn Berry는 node_modules 폴더 없이도 패키지를 관리할 수 있도록 설계되어, .yarn/cache 폴더에 패키지를 압축된 .zip 형태로 저장한다. 이를 통해 모든 의존성을 한 곳에서 직접 관리한다.
Pnpm의 해결 방법
pnpm은 위 문제를 해결하기 위해 프로젝트에서 사용하는 의존성 패키지(dependencies/devDependencies)는 node_modules 폴더 바로 아래에 해당 패키지의 심볼릭 링크를 만든다.
근데 node_modules 아래에 있는 express가 express@4.17.3의 심볼릭 링크가 아니라 express@4.17.3/node_modules/express 의 심볼릭 링크인 이유는 무엇일까? 왜 express 패키지가 express 안에 또 존재하는 것일까?
pnpm 공식 문서를 살펴보면 pnpm은 content-addressable store 방식을 사용해 디스크 공간을 절약하고자 했다. content-addressable store 방식은 전역 스토어에 종속성을 설치하고 심볼릭 링크와 하드 링크를 사용해서 디렉토리를 구성하는 방식이다. 전역 저장소에서 패키지를 공유하는 구조를 사용한다.
하드 링크(Hard Link) vs 심볼릭 링크(Symbolic Link or Soft Link)
리눅스 계열에서는 링크가 하드 링크와 심볼릭 링크 2가지로 나뉘어져 있다. 하드 링크와 심볼릭 링크를 이해하기 위해서는 inode 개념을 이해해야 한다. 모든 파일과 디렉토리는 inode를 가지고 있고 이 inode에 파일의 허가권, 소유권 등의 정보를 저장한다.
하드 링크는 원본 파일과 동일한 inode를 직접적으로 가리킨다. 심볼릭 링크는 inode를 복사한 뒤 복사한 inode를 가리킨다.
Yarn Berry와 Pnpm 중에서는 무엇을 사용해야 할까
일단 pnpm 사용할 것 같아서 그냥 yarn berry의 문제점을 찾아보았다. yarn berry를 사용하다가 pnpm으로 전환한 사람들의 후기를 검색했고, 다음과 같은 글을 발견할 수 있었다.
글의 내용을 정리하면 yarn berry는 zero install일 경우 의존성 관리를 git으로 하기 때문에 .git 디렉토리가 무거워지고 git의 체크아웃 속도에 영향을 줄 수 있다고 했다. 또한 PnP이 node.js의 의존성 관리 방식과 다르기 때문에 다른 패키지랑 잘 호환되지 않는 문제도 발생했다고 한다. PR을 할 때도 의존성이 변경되면 해당 파일들도 PR에 뜨기 때문에 코드 리뷰를 하기 불편해지는 문제점이 있다.
pnpm의 경우 단점을 딱히 찾아보지는 않았지만… 우선 장점 같은 경우에는 의존성을 로컬 디렉토리에 캐싱하기 때문에 이미 캐싱한 적이 있는 패키지일 경우 빠르게 다운로드를 받을 수 있다고 한다.
결론
그냥 pnpm 사용하자.
참고
https://medium.com/wantedjobs/yarn-classic에서-pnpm으로-전환하기-with-turborepo-7c0c37cb3f9e
https://toss.tech/article/node-modules-and-yarn-berry
https://inpa.tistory.com/entry/LINUX-📚-하드-링크hard-link-심볼릭-링크symbolic-link-아이노드inode