Lighthouse of FE biginner

pnpm 톺아보기 본문

[WEB] 프론트엔드

pnpm 톺아보기

[FE] Lighthouse 2024. 9. 8. 12:46

Overview

지난 7월 프로젝트의 프론트엔드를 개편하는 과정에서 패키지 매니저를 Yarn berry에서 pnpm으로 교체를 진행했습니다.
그 당시 교체를 선택한 이유는 몇 가지가 있었는데, 이번 포스팅에서는 pnpm을 학습했던 내용을 기록하고 교체를 진행했었던 이유를 남겨보려고 합니다.
 

pNpM

먼저 pNpM에 대해서 살펴봅시다.

왜 pnpm을 pNpM이라고 했는지 간단하게 살펴보자면, 홈페이지에서 새로고침을 할 때 마다 텍스트가 랜덤하게 변하는 것을 확인 했습니다. 딱히 정해져 있지 않은 것인가 해서 어느 순번에 나타난 pNpM을 살펴보려고 합니다.

 
pnpm은 perfomant npm 의 약자로써 말 그대로 향상된 npm입니다. 그럼 어떤 장점이 있길래 향상된 npm 이라고 하는 것 일까요? 
 

전역 스토어, 가상 스토어, node_modules를 활용한 디스크 효율성, I/O 퍼포먼스 개선

먼저 pnpm의 원리를 살펴봅시다.
 
pnpm을 통해 최초로 라이브러리를 설치하게 된다면 해당 라이브러리는 디스크에 단 하나만 존재하는 저장소(Content-addressable store)에 저장됩니다.
 
저장소의 모습을 살펴볼까요? 터미널을 열어서 디스크에 저장된 저장소의 위치를 확인해봅시다.

pnpm store path

 
저는 다음의 위치를 얻었습니다. (/Users/thomas/.pnpm-store/v3)  스토어로 이동해봅시다.

cd /Users/thomas/.pnpm-store/v3

 

 
files 라는 폴더가 존재합니다. 해당 폴더로 이동을 하면 숫자와 해쉬 값으로 된 폴더들이 존재합니다. 해당 폴더들은 그동안 다운로드 받았던 폴더의 목록입니다.
 
실제 프로젝트의 node_modules 폴더에는 .pnpm이라는 가상 스토어가 존재하고, 해당 가상 스토어에는 설치한 라이브러리 패키지에서 사용하는 의존성들을 저장소에 저장된 패키지들을 버전별로 하드링크해 사용합니다. 아래 스크린샷을 살펴보면 node_modules 폴더 하위의 .pnpm 이라는 가상 저장소에서 스토어에 설치된 패키지를 하드링크한 라이브러리 목록이 보입니다. 해당 라이브러리들은 라이브러리와 버전 별로 하드링크 되어 있습니다.

 
그리고 실제 node_modules 폴더에서는 사용하는 의존성(라이브러리)를 평탄하지 않게 사용하고 있습니다. node_modules에서는 가상 저장소에 저장된 라이브러리를 소프트 링크(심볼릭 링크)로 사용하고 있습니다.

vscode에서는 라이브러리의 우측에 링크 표시를 확인할 수 있습니다.

 
정리하자면 pnpm은 저장소, 가상 저장소, node_modules 를 활용해 라이브러리를 관리합니다. 실제 바이너리 파일들은 전역 저장소에 저장이 되며, 가상 저장소(.pnpm)는 저장소에 저장된 라이브러리 파일을 하드 링크를 활용해 저장합니다.
프로젝트의 node_modules는 가상 저장소에 하드 링크된 라이브러리를 소프트 링크(심볼릭 링크)를 통해 라이브러리를 사용하게 됩니다.
 
하드 링크된 라이브러리들은 가상 저장소에서 버전별로 평탄하게 관리가 되고, node_modules에서 해당 의존성들을 심볼릭 링크로 사용해 디스크를 효율적으로 관리하며, 하드 링크와 소프트 링크 (링크 시스템)를 함께 활용해 디스크 I/O 퍼포먼스를 향상 합니다.

하드링크
사본과 같은 역할을 한다. 원본 파일이 삭제되어도 하드링크에는 해당 파일의 데이터가 포함되어 있어서 데이터에 엑세스가 가능하다. 원본 파일과 같은 inode를 가진다. 그래서 여러 하드링크를 만들더라도 디스크 용량을 차지하지 않는다. 디렉토리에는 하드링크를 허용하지 않는다.

inode란?
inode는 index node의 줄임말로서 리눅스 파일 시스템에서 파일이나 디렉토리의 메타 데이터를 저장하는데 사용된다.

소프트 링크(심볼링 링크)
윈도우의 바로가기와 같은 개념 (포인터의 개념)이다. 주소를 가리키고 있기 때문에 원본 노드가 삭제될 경우 동작이 불가능하다. 하드링크와 다르게 원본과 다른 inode를 가지며 디렉토리에도 소프트링크를 사용할 수 있다.

 

엄격한 의존성 관리

위에서 pnpm이 패키지를 관리하는 방법을 살펴봤습니다. 가상 스토에에서 버전 별로 의존성을 평탄하게 관리하며 node_modules 에서는 의존성을 평탄하게 관리하지 않고 사용하는 버전을 명확하게 합니다.
이로 인해서 의존성 관리를 엄격하게 할 수 있으며, npm과 yarn classic에서 발생했던 팬텀 디펜던시 문제를 해결할 수 있게 됩니다.
 
간단하게 팬텀 디펜던시에 대해서 알아봅시다.
 
npm v3 이하에서는 사용하는 패키지를 node_modules에서 계층적으로 관리했습니다. 만약 a라는 패키지가 b라는 패키지를 사용하고 b 패키지에서는 c패키지를 사용한다면 아래와 같은 경로가 만들어집니다.

.node_modules/a/.node_modules/b/.node_modules/c/.node_modules .....

 
실제로 이렇게 깊게 들어가는 계층 구조로 인해 디스크의 저장 효율성과 I/O 퍼포먼스가 심각하게 저해 됐습니다. Windows 파일 시스템에서는 경로의 길이를 260자로 제한하고 있다는 문제점도 있었습니다. 하여 npm v3 이후부터는 프로젝트에서 사용하는 의존성을 node_modules 폴더의 루트에서 평탄하게 관리하기로 했습니다. 이를 의존성 목록을 끌어올리는 듯한 형상으로 호이스팅이라고 부르게 됐습니다.
 
호이스팅으로 인해 디스크 효율성과 I/O 문제는 어느정도 해결을 했지만 다른 문제가 발생했습니다. 실제로 설치하지 않은 버전, 설치하지 않는 라이브러리를 사용할 수 있게 됐다는 점 입니다.
 
a라는 패키지를 설치했는데, a 패키지는 b 패키지를 의존성으로 사용하고 있습니다. 하여 node_modules 루트에 b 패키지도 설치가 됐습니다. 프로젝트를 진행하는 도중 설치하지 않은 b 패키지를 발견할 수 있습니다. 또한 a 패키지에서 import 해야 하는 것을 이름이 같아서 b 패키지에서 import 하는 케이스도 발생합니다. 프로젝트가 진행되면서 a 패키지를 사용하지 않게 됐고, a 패키지를 삭제합니다. 이때 b 패키지도 함께 삭제가 되면서 b 패키지에서 import 했던 코드가 에러를 유발합니다. 이를 유령 의존성(설치하지 않는 패키지에 디펜던시가 생겨) 팬텀 디펜던시 라고 부릅니다.
 
pnpm에서는 계층적으로 의존성을 관리하고 버전별로 의존성을 엄격하게 관리하기 때문에 위 팬텀 디펜던시 문제를 해결할 수 있습니다.
 

쉽게 적용하는 모노레포

npm, Yarn classic, Yarn berry 모두 모노레포를 위한 workspace 기능을 제공하고 있습니다.
강의를 통해 세 패키지 매니저의 workspace를 살펴봤지만, 가장 간편하다고 느꼈던 패키지 매니저는 pnpm 입니다.
pnpm을 활용해 모노레포를 셋팅하려면 pnpm-workspace.yaml 파일만 프로젝트의 루트에 만들어주면 됩니다. 아래와 같이 yaml 파일에 패키지를 명시해주면 모노레포를 간단하게 셋팅할 수 있습니다.

packages:
  - "apps/*"
  - "packages/*"
  - "server"

 
pnpm에서는 --filter 옵션을 활용한다면 모노레포를 쉽게 제어할 수 있다는 장점도 존재합니다.

pnpm --filter @career-up/edu add packages
pnpm --filter @career-up/ui-kit build
pnpm --filter @career-up/shell dev

 

Yarn Berry > Pnpm

pnpm에 대해 간단하게 살펴봤습니다. 그럼 프로젝트에서 패키지 매니저를 Yarn berry에서 pnpm으로 교체한 이유를 간단하게 살펴보겠습니다. 저희 프로젝트에서는 Yarn Berry pnp와 zero install 기능을 사용하고 있었습니다.

 

프로젝트가 진행될수록 깃에 가해지는 부하

Yarn berry PNP를 사용하면서 Zero Install 기능을 사용해 깃을 통해 의존성 관리를 하고 있었습니다.
이는 의존성을 zip파일을 활용해 깃에서 코드와 함께 형상 관리를 하는 기능입니다. 진행중인 프로젝트는 5년이 넘게 유지보수 중인 오랫동안 진행된 프로젝트 입니다. 그동안 다양한 패키지들을 설치하면서 코드 베이스 용량이 커져가고 깃에 저장된 의존성 덩어리가 비대해져 깃을 통해 프로젝트를 형상 관리하는데 어려움을 겪기 시작했습니다.
또한 Zero Install을 지원하지 않는 패키지들도 존재했고 이로 인해서 완벽한 Zero Install이 어렵다는 결과도 확인을 했습니다.

러닝 커브

Yarn Berry의 PNP 동작 방식은 기존 패키지 매니저인  npm, yarn classic과는 다릅니다. 패키지 매니저의 동작 방식을 이해하려면 상당 시간을 투자해야 한다는 점이 있었고 이는 프로젝트를 진행하는 인원들에게 부담을 주고 있었습니다.

pnpm의 편리함

새로운 대안이 주는 편리함이 교체의 이유가 될 수 있을까요? 패키지 매니저를 교체할때 쯤 pnpm의 매력에 빠져가고 있었습니다. 너무 간단했고 퍼포먼스 또한 훌륭했습니다. 어차피 프로젝트를 전반적으로 개선하는 과정에서 쉽게 교체할 수 있다면 교체하지 않을 이유가 없다고 생각했습니다.

모노레포의 도입

저희 프로젝트는 추후 모노레포와 프로젝트 아키텍처의 구조를 MFA로 변경할 목표를 세우고 있었습니다. 모노레포를 쉽고 효율적으로 사용하기 위해 여러 모노레포 툴을 고민하던 중 Turborepo에 대한 고민을 하게 됐고, Turborepo는 패키지 매니저를 pnpm을 사용하는 것을 권장하고 있다는 점을 캐치 했습니다.
 

결국은

결국은 Yarn Berry를 걷어내고 pnpm을 도입했습니다.
.yarn 폴더를 제거하고 yarn과 관련된 모든 것들을 삭제한 후 pnpm을 통해 패키지를 설치했습니다. 저희 프로젝트에서는 생각보다 패키지 매니저를 교체하는 과정이 단순하고 간단했기에 특별한 트러블슈팅을 하지 않아도 됐습니다.
 
패키지 매니저를 변경한다면 고려해야 할 몇 가지 점들이 있습니다.
먼저 팀원들에게 가이드를 명확하게 해줘야 한다는 점 입니다. 프론트엔드 개발자는 익숙한 과정이기에 별 탈 없이 pnpm을 적용할 수 있습니다. 하지만 백앤드 개발자나 프론트엔드에 익숙하지 않는 개발자들에게는 pnpm을 활용해 프로젝트를 띄우는 방법을 명확하게 가이드 해야 합니다.
그리고 패키지 매니저를 교체 했기 때문에 CI/CD 스크립트나 빌드 스크립트를 변경해주어야 합니다.
 
위 두 가지를 고려하지 않는다면 반쪽뿐인 패키지 교체가 될 수 있기 때문에 교체 시점에 액션 아이템으로 꼭 짚고 넘어가야 합니다!
 

마치며

간단하게 pnpm에 대해서 살펴보고 패키지 매니저를 교체한 이유를 살펴봤습니다.
이번 포스팅은 사실 제 스스로 pnpm에 대해서 공부했던 내용을 정리하려고 작성한 포스팅입니다.
다른 의견이 있으시다면 댓글을 남겨주셔도 좋습니다. 환영입니다!
 
pnpm을 공부하고 계신 분들에게도 많은 도움이 됐으면 좋겠습니다.
 
읽어봐주셔서 감사합니다.