yarn classic에서 pnpm으로의 시작
본 글에 앞서, 해당 포스팅은 개인의 유령의존성 경험을 토대로 yarn classic에서
pnpm을 사용하게 된 계기를 설명합니다. 패키지 매니저에 대한 딥다이브가 아닙니다.
1️⃣ yarn classic이 쏘아올린 작은(사실 겁나 큰) 공, 유령의존성
저는 이전 회사도, 현재 회사도 yarn classic으로 프로젝트를
운영하고 있었고, 다른 패키지 매니저 경험은 없었습니다.
현실에 안주해 있는 동안 yarn berry, pnpm이라는 새로운 패키지 매니저들이 등장했지만
좋다는 말만 듣고 프로젝트에 적용은 하지 않고 있었는데요. 어느 날, 프로젝트를 진행하며
제가 설치하지 않은 의존성이 어디선가 추가되어 하나둘씩 발생하기 시작한 문제로 인해 반강제로
패키지 매니저에 대한 관심을 가지게 되었습니다.
react-slick으로 인한 유령 의존성이 문제의 시작이었어요.
(당시 yarn 1.22.19 버전을 사용하고 있었습니다.)
react-slick 대신 swiper로 변경하며 실수로 @types/react-slick 을 남겨뒀는데,
나중에 chakra UI의 Slider라는 컴포넌트를 import 하기 위해 Slider를 작성했더니
뜬금없이 react-slick의 Slider가 import 되었습니다.
VSCode에서 코드를 작성할 때는 문제가 발생하지 않았어요.
즉, 컴파일 시점에서는 TypeScript가 모든 의존성을 확인하고 오류를 발생시키지 않았습니다.
그러나 코드를 저장하고 프로젝트를 실행했을 때, 런타임 시점에서 오류가 발생했는데요.
이는 설치되지 않은 라이브러리가 실제로 코드에서 참조되었기 때문입니다.
처음엔 막연하게 왜 안 될까?라는 생각에서 문제를 이해해보려 했습니다.
컴파일 시점에서는 문제가 없는데.. 왜 런타임 시점에서 에러가 발생하지?
설치되지 않은 라이브러리인데 어떻게 import가 가능한 것인지는 둘째 치고
이건 보통 문제가 아니라고 느꼈습니다.
프로젝트에서 직접 의존하지 않는 패키지인 react-slick을 어디서 참조하는 건지
알 수가 없었고 과연 react-slick과 관련된 것만 문제일까?, 프로젝트 전반적으로
다른 의존성 관련 문제가 발생할 수도 있지 않을까 하는 불안감에 휩싸이게 되었습니다.
이 때부터 프로젝트의 의존성 관리가 중요하다는 것을 깨닫고 패키지 매니저에 대해 알아보기 시작했습니다.
2️⃣ 유령의존성은 뭐고 왜 발생하는걸까?
하나의 패키지는 관련된 여러 패키지를 의존성으로 긁어와 생성된다는 것을 처음 알게되었는데요.
위의 Dependency Tree 이미지처럼 npm과 yarn classic에서는 node_modules의 중복 설치 문제를
해결하기 위한 방법으로 hoisting을 사용하며 하나의 루트에 패키지들을 평평하게 위치시켰습니다.
하지만 hoisting으로 인한 문제가 발생하는데요.
위의 이미지에서 패키지 D는 B의 2.0 버전을 사용하지만, package-1의 의존성 트리에서는
B의 1.0 버전이 사용되고 있습니다.
B의 2.0 버전에서만 가능한 기능을 사용중이라면 B의 1.0 버전을 사용하는 A와 C는 D와 맞지 않겠죠.
이 부분에서 예기치 못한 문제가 발생할 수 있습니다.
즉, 프로젝트에서 직접 의존하지 않는 패키지를 암묵적으로 참조하게 되는 경우가 발생한다는 것인데요.
이를 유령 의존성이라고 말하더라고요.
유령의존성을 해결하기 위해 정기적인 의존성 검사를 시행하기엔 불필요한 공수가 들어갔고
필히 리소스 낭비로 이어질 것이라 판단했습니다.
유령의존성을 걱정하지 않고 성능까지 개선된 패키지 매니저가 필요한 상황에서
yarn berry와 pnpm이라는 선택지가 주어졌습니다.
3️⃣ 의존성 관리과 성능 개선을 둘 다 잡을 수 있는 pnpm
위의 예시를 몇 차례 경험하고 나서 몇개월 후, 신규 프로젝트에서 초기 세팅부터 진행할 수 있는 기회가 주어졌고
패키지 매니저로 무엇을 선택할지에 대한 고민 후, pnpm을 선택했습니다.
pnpm은 node_modules를 직접 설치하는 대신, 전역 저장소(Virtual Store)에서
패키지를 공유하는 구조를 사용합니다 pnpm이 패키지를 설치할 때, package.json에 명시된
패키지를 읽은 후 node_modules에 Symbolic Link를 생성하여
전역 저장소의 해당 패키지를 참조한다고 많이들 설명해주셨는데요.
개인적으론 위의 내용이 와닿지 않아 react-slick과 동일한 의존성 패키지를 갖는
B라는 라이브러리를 사용했을 때로 이해하려 해봤습니다.
react-slick 설치 시 global store에 원본 react-slick 패키지를을 저장한 후
.pnpm 디렉토리에는 Hard link로 파일을 복사함과 동시에 node_modules에는
.pnpm 디렉토리 안에 위치한 react-slick을 가리키는 Symbolic link를 생성합니다.
저의 경우엔 global store의 path는 로컬환경 > Library > pnpm > store > v3 > files였습니다.
react-slick은 많은 파일들 중 하나겠네요.
node_modules에 심볼릭 링크가 생성되고, .pnpm 디렉토리에 하드 링크가 생성됩니다.
이후 react-slick과 동일한 의존성 패키지가 존재하는 라이브러리인 B를 설치할 때,
global store에서 이미 패키지가 존재하는지 체크한 후 .pnpm 디렉토리에 Hard link가 존재한다면
기존의 패키지를 재사용함과 동시에 node_modules에서 동일한 의존성 패키지에 대한
새로운 Symbolic link가 생성되어진다고 이해했습니다.
// react-slick 설치 시
~/.pnpm-store/v3/files/ (global store)
└── react-slick 원본 파일들
프로젝트/
└── node_modules/
├── react-slick -> ../.pnpm/react-slick/node_modules/react-slick (Symbolic Link)
└── .pnpm/
└── react-slick/
└── node_modules/
└── react-slick/ (Hard link)
---
// 공통 의존성 패키지를 갖는 라이브러리 B 설치 시
프로젝트/
└── node_modules/
├── react-slick -> ../.pnpm/react-slick/node_modules/react-slick
├── libraryB -> ../.pnpm/libraryB/node_modules/libraryB
└── .pnpm/
├── react-slick/
│ └── node_modules/
│ └── react-slick/ (기존 Hard link 재사용)
└── libraryB/
└── node_modules/
└── libraryB/ (새로운 Hard link)
Hard link와 Symbolic Link를 이용하여 파일 복사를 최소화한 패키지 설치 속도 비교 및 빌드 속도 향상 측정은 가능했지만
디스크 공간 절약은 어느 정도 이뤄졌는지 파악을 하지 못한 점이 아쉬웠습니다.
4️⃣ yarn berry vs pnpm
둘 중 하나를 선택하기 위한 고민을 할 때 Remember와 Hackle의 tech blog를 참고하여 결정을 내렸는데요.
프로젝트에 필요한 패키지와 버전이 .yarn/cache 디렉토리에 압축된 zip 파일로 저장되어 있고
.yarn/cache 디렉토리가 git에 포함되며 install이 필요없어지는 zero install이 좋아보여
yarn berry를 선택하려고 했습니다.
하지만 zero install을 사용한다면 .git 디렉토리에 모든 파일 기록을 저장하니
자연스레 .git 디렉토리가 무거워질 것이고, 의존성 하나가 변경될 때 연관되어 있는 다른 의존성 파일들도
함께 변경이 발생해 코드 리뷰가 빡세지겠다는 생각이 들었습니다.
yarn 4버전으로 업데이트되며 성능이 크게 개선되며 그에 따라 zero install을 사용하지 않아도
pnpm과 엇비슷한 기대효과를 누릴 수 있겠다 여겨졌지만 이미 마음이 pnpm으로 기운 상태였습니다.
물론 현 시점에서는 소규모 프로젝트라 무엇을 선택해도 상관없지만 후일을 도모했을 때 무엇이
DX 개선을 이끌어낼 수 있을까라고 묻는다면 pnpm이 나아보였습니다. 그래서 pnpm을 선택했어요.
5️⃣ 의존성을 생각한 적용이었지만 현 시점에서 모노레포까지 필요한가?
예전부터 기존 서비스와 현재 서비스를 모노레포로 운영할 필요가 있을까 싶었는데요.
pnpm을 사용한다고 꼭 모노레포로 운영을 해야한다는 것은 오히려 편견으로 다가왔기 때문입니다.
규모가 거대한 프로젝트라면 모노레포와 같은 시스템을 적용하는 것이 타당하지만
이제 갓 mvp 단계를 지나고 있는 프로젝트에 모노레포를 도입하는 것은
배보다 배꼽이 더 큰 경우라고 여겨졌습니다.
동료와 논의 후 모노레포를 적용하는 것은 미뤄두고 pnpm의 장점만 사용하되
우선은 단일 레포로 운영하자고 제안했습니다.
후기
이번에는 저도 기깔나게 기업 테크 블로그처럼 멋진 포스팅을 하고 싶었지만
현실적으로 제 개발 가방끈도 짧고.. 이상하게 블로그를 작성할 땐 자존심을 부리게 되어
글을 복사 붙여넣기 하는 것도 싫었습니다. 그래도..경험기잖아요.. 적당히 감안하고 봐주셔요.
Reference
개발자 단민 | 패키지 매니저, 뭘 쓸까? (npm, pnpm, yarn, yarn berry, etc.)
리멤버 웹 서비스 좌충우돌 Yarn Berry 도입기 - Remember & Company
우리는 하나다! 모노레포 with pnpm #우아콘2022 #Day2_음식그이상의것을문앞으로
[패키지매니저를 알아보자] NPM, YARN, YARN BERRY, PNPM