FE

pnpm이 뭔가요?

jvn4dev 2024. 9. 18. 16:51

평소 구현에 대한 부분만 관심을 갖고 개발하다보니 패키지 매니징과 관련된 에러나 의존성 관련된 문제를 만났을 때 어려움을 느꼈다. 

현업에서의 프로젝트말고도 개인 프로젝트를 진행하면서도 몇가지 패키지 매니저를 사용해봤지만 단순히 빨라서 사용했지 정확히 어떤 이유에서 사용하는지 정확히 알고 사용하진 않았던 것 같다.

패키지 매니징 관련해서 문제를 만났으니 현재 현업에서 사용 중인 pnpm에 대해서 조금 알아보고 가면 좋을 것 같아 가볍에 정리해보려고 한다.


우선 pnpm의 공식문서에서는 다음과 같이 소개하고 있다.

  • npm대비 2배 빠르다.
  • 효율적이다.(?) - node_modules cloned, hard linked? 
  • 모노레포를 지원한다.
  • 엄격하다.

이들이 말하는 효율적이다는 부분은 처음 봤을 때 정확하게 이해하지 못했다. 이는 잠시 후에 다시 알아보도록 하자.

 

이제 그럼 pnpm을 이용해서 pnpm install을 했을 때 어떤 일들이 일어날까?

  1. pnpm-lock.yaml 파일 분석
  2. 종속성 해결 (dependency resolution)
  3. 패키지 다운로드
  4. 하드링크/심볼릭 링크(이하 심링크) 생성(효율적이다는 부분과 연관이 있어보인다?)
  5. 종속성 설치
  6. 패키지 스크립트 실행
  7. 심링크 및 실행 파일 설정
  8. 캐시 업데이트
  9. 상태 로그 보고

이렇게 총 9가지 정도의 일들이 일어난다고 할 수 있다. 별 생각 없이 사용해왔던 일인데 생각보다 많은 일들을 해주고 있어서 놀랐다. 하나씩 더 어떤 일들이 일어나는지 확인해보자.

 

1. pnpm-lock.yaml 파일 분석

프로젝트 루트 상의 존재하는 pnpm-lock 파일을 그래프 분석한다.

해당 분석을 통해 필요한 모듈의 의존성을 분석하고 정확한 종속성 버전과 하위 종속성 버전을 분석한다.

이는 pnpm-lock.yaml 파일의 설치된 모듈의 하위 목록 중 version부분을 확인하면 확인이 가능하다.

next:
  specifier: 14.2.3
  version: 14.2.3(@babel/core@7.24.6)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1)

 

여기서 볼 수 있듯이 next의 14.2.3 버전은 babel, babel-plugin-macros, react-dom 등에 의존하고 있는 것을 확인할 수 있다. 여기서 괄호가 중첩되는 부분은 해당 하위 모듈이 의존하고 있는 하위 모듈까지 표시한다.

여기서 만약 루트 상의 pnpm-lock 파일이 존재하지 않는다면 다음 과정인 종속성 해결(dependency resolution) 과정이 시작된다.

 

2. 종속성 해결 (dependency resolution)

package.json의 종속성 목록 중 가장 최신 버전을 선택하고 종속성 트리를 구축한다. 그리고 이 과정에서 생기는 충돌을 해결한다.

 

3. 패키지 다운로드

종속성 목록이 성공적으로 생성됐다면 해당 패키지들을 npm registry에서 다운로드한다. 

이 과정에서 pnpm은 해당 패키지들의 중복 설치를 방지하기 위해서 글로벌 스토어에 모든 패키지들을 저장한다. 프로젝트의 패키지가 설치되는 글로벌 스토어 경로 확인은 다음 커맨드를 통해 확인할 수 있다.

pnpm store path

// 해당 스토어 내 사용하지 않는 패키지 제거
pnpm store prune

 

만약 해당 스토어에 이미 패키지가 존재한다면 다운로드하지 않고 바로 사용한다.

 

4. 하드링크/심볼릭링크 생성

pnpm은 스토어에 설치된 패키지들을 프로젝트 내부에 설치하지 않는다.

대신 하드링크와 심볼릭 링크를 활용하는데 이들에 대해서 간단하게 알아보고 가자.

하드링크에 대해 설명하려면 iNode에 대해 먼저 알아야한다.
iNode란?
리눅스 파일시스템에서 파일과 디렉토리를 식별하고 관히라는데 사용하는 데이터 구조이다.
각종 metadata를 포함하여 소유자, iNode 번호, 크기, 주소 등의 데이터를 포함한다.
ls 명령어 중 -i 옵션을 사용하여 해당 iNode 번호를 같이 확인할 수 있다.
ex) 6291630 drwxr -xr -x 105 ... 에서 6291630이 해당 파일 혹은 디렉토리의 iNode 이다.

하드링크란?
리눅스/유닉스 기반의 os에서 파일에 대한 추가 파일을 생성하는 메커니즘이다.
iNode 번호를 공유하는 두개의 파일 엔트리를 생성한다.

하드링크의 특징으로 원본과 새로운 파일의 iNode를 공유하여 같은 파일이지만 다른 이름을 갖도록 하고 이를 통해 디스크 공간을 절약한다. 
하드링크를 생성해도 실제 데이터는 하나이기 때문이다. 
이 때 링크 수가 0이 아니라면 원본을 삭제해도 기존 데이터는 유지된다. 참조 원리와 같아 원본이 수정되면 다른 모든 링크에도 변경사항이 적용된다.
파일이 아닌 디렉토리의 경우는 하드링크 생성이 불가능하다. 이는 디렉토리 하위의 트리 구조의 경우 순환참조가 발생할 수 있기 때문이다.

 

심링크란?
리눅스/유닉스 기반의 파일에 대한 간접적인 참조를 만드는데 사용된다.
심링크는 원본의 실제 데이터가 아닌 경로만을 갖고 원본을 참조한다.

심링크의 특징으로 파일 크기가 작다. 이유는 정말 경로를 데이터 자체로 갖기 때문이다. 그래서 경로의 길이에 따라 크기가 달라진다.
그리고 하드링크와 다르게 iNode를 공유하지 않는다. 원본의 iNode 번호를 공유하지 않고 심링크 자체적인 별도의 metadata를 갖는다.
원본이 삭제가 되면 유효하지 않고 심링크를 삭제하면 원본은 그대로 유효하다.
또 하드링크와 다르게 디렉토리에 대한 심링크 생성이 가능하다. 경로를 갖는 이유로 다른 파일/디렉토리를 가르키는 링크간의 순환참조가 가능하다.

 

이를 통해 글로벌 스토어에 설치된 패키지들을 node_modules 폴더에 하드링크로 연결하여 저장공간을 절약하고 속도를 높인다. 이를 통해 하나의 패키지를 여러번 사용하는 경우에도 하나만 설치하면 바로 끝이라고 할 수 있다. 

 

5. 종속성 설치

하위 종속성을 재귀적으로 설치하고 node_modules내 패키지를 설치하고 배치한다. 

 

6. 패키지 스크립트 실행

종속성 설치가 끝나면 pnpm의 라이프사이클에 따라 스크립트가 실행된다.

preinstall, install, postinstall, preuninstall, uninstall, postuninstall, prepublish, prepare 등...

여러가지가 존재하고 현재 현업 프로젝트에서는 preinstall,postinstall, prepare를 활용하여 국제화를 위한 로컬라이징 시트의 번역 파일을 불러와 모노레포 내부 여러 프로젝트 내 public 경로에 언어별 json 파일을 배치하는 등의 처리를 하고 있다.

 

7. 심링크 및 실행 파일 설정

설치된 패키지의 실행파일을 node_modules/.bin 폴더에 심링크하여 cli 명령어 사용이 가능하다. 

 

8. 캐시 업데이트

패키지의 해시와 metadata는 캐시에 저장된다. 이를 통해 동일한 패키지라고 해도 변경여부에 대한 확인이 가능해진다.

이후 pnpm install 시에 반복된 불필요한 작업을 하지 않아 속도가 더 빠르다.

 

9. 상태 로그 보고

위 일련의 모든 과정이 끝나면 비로소 우리가 커맨드창을 통해 결과를 보고 받게 된다.

이 내용에는 패키지 버전과 충돌 해결 정보 등이 노출된다.


단순히 npm보다 빨라서 사용하기엔 너무 많은 일들을 하고 있어 이번 계기로 pnpm이 어떤 녀석인지 조금은 알게되었다. 구현에 대한 부분도 중요하지만 이런 devops 적인 부분들도 필요한 역량이라고 생각되고 욕심이 좀 생긴다.

조금씩 관련된 문제가 발생하면 꾸준히 정리하고 공부하는 습관을 들여야겠다. 틈틈히 다른 패키지 매니저들도 어떤 부분이 다른지 궁금증도 생겨 하나씩 사용하게되면 정리해봐야겠다.