yarn berry 와 pnpm 중 패키지 매니저 고르기 (1)

nyoung·2024년 5월 15일
0
post-thumbnail

차세대(?) 패키지 매니저로 선택받는 yarn berry와 pnpm 중 뭘 선택해야 할까?

패키지 매니저 별로 속도나 방식이 다르기 때문에, 빠른 빌드 속도와 안정성 등을 고려해 기본 패키지 매니저인 npm에서 yarn berrypnpm으로 변경하는 프로젝트들이 많다.

나 역시 회사에서는 npm => yarn berry 로 변경했었는데, 사이드 프로젝트를 진행할 때 yarn berrypnpm 중 뭘 써야할 지 고민하는 시간을 가지게 되었다.

들어가기 전에) 왜 npm에서 변경하는 걸까?

npm은 Node.js의 표준(기본) 패키지 매니저이다. 즉, 별도로 설치할 필요가 없다. 편하다.
프로젝트를 시작할 때 항상 npm을 이용한다. (무지성 npm install ...)

그럼 왜 편한 npm을 놔두고 다른 패키지 매니저로 변경을 굳이 해야 하는걸까?

왜냐하면 안전성, 속도 측면에서 비효율적인 부분이 있기 때문이다.

다음을 통해 그 비효율적인 측면을 알아보자.

1. node_modules 탐색 비효율

node_modules 구조 하에서 모듈을 검색하는 방식은 디스크 I/O 작업이다.(파일 시스템을 기반으로 의존성을 관리한다.)

모듈 탐색을 메모리 상에서 자료구조로 처리하지 않고 I/O로 직접 처리하다보니 느린 I/O 호출을 반복하고, 경우에 따라서는 중간 실패도 일어나게 된다.

NPM은 패키지를 찾기 위해 계속 상위 디렉토리의 node_modules를 탐색한다. 따라서 패키지를 바로 찾지 못할수록 I/O 호출이 반복된다.

2. 환경에 따라 달라지는 동작

NPM은 패키지를 찾지 못하면 상위 디렉토리의 node_modules 폴더를 계속 검색한다.
이 특성 때문에 어떤 의존성을 찾을 수 있는지는 해당 패키지의 상위 디렉토리 환경에 다라 달라진다.

이렇게 환경에 따라서 달라지게 되면 제 컴퓨터에서는 되는데요,, 같은 까다로운 디버깅을 일으킬 수도 있다.

3. 비효율적인 설치, 느린 속도

NPM에서 구성하는 node_modules 디렉토리 구조는 매우 큰 공간을 차지한다.
일반적으로 간단한 CLI 프로젝트도 거대한 node_modules 폴더가 필요하다.
용량만 많이 차지할 뿐 아니라, 큰 node_modules 디렉토리 구조를 만들기 위해서는 I/O 작업이 필요하다. 또한 npm은 순차적인 설치로 소요시간이 오래걸린다.

4. 유령 종속성

Untitled

npm(v3)은 v2까지의 중복 의존성 문제(중첩된 패키지 구조 때문에 같은 패키지가 여러개 설치되는 문제)와 속도 문제를 개선하기 위해 호이스팅 메커니즘을 도입했으나, 이 때문에 유령 의존성 문제가 생긴다.

my-project/
├── node_modules/
│   ├── package-A/
│   │   ├── node_modules/
│   │   │   └── package-C@1.0.0/
│   │   └── package.json
│   ├── package-B/
│   │   ├── node_modules/
│   │   │   └── package-C@1.0.0/
│   │   └── package.json
│   └── package-C@1.0.0/
└── package.json

위 폴더 구조를 봤을 때, C 패키지는 중복으로 3번 설치된다. 이는 디스크 공간을 비효율적으로 사용하게 된다.

따라서 호이스팅 메커니즘을 도입했다. 호이스팅 매커니즘이란, 패키지 매니저가 가능한 한 최상위 node_modules에 패키지를 설치해 flat한 구조로 만드는 것이다.

위 그림과 같이 npm, yarn classic은 중복 설치를 방지하기 위해 종속성 트리 아래에 존재하는 패키지들을 호이스팅 및 병합한다. 그렇게 되면 패키지 최상위에서 트리 깊이 탐색하지 않고 루트 경로에서 원하는 패키지를 탐색할 수 있다.

하지만 이렇게 되면 직접 설치하지 않고 간접 설치한 종속성에 개발자가 접근할 수도 있다. (위 예제에서, 개발자가 직접 설치하지 않은 B(1.0) 패키지에 직접 접근이 가능하다)

따라서 존재하지 않는 종속성에 의존하는 코드가 발생할 수 있다.이를 유령 의존성이라고 한다.

유령 의존성 현상일 발생할 때 package.json에 명시하지 않은 라이브러리를 조용히 사용할 수 있게 된다. 다른 의존성을 package.json에서 제거했을 때 조용히 같이 사라져 유령 의존성을 사용하는 코드가 있을 때(있어서는 안되지만) 에러를 발생시키기도 한다.

대안 1. yarn berry (yarn v2 이상)

PnP(Plug’n’Play)

yarn berry는 PnP라는 기술을 이용해 node_modules 기반의 문제점을 해결한다.

yarn berry는 node_modules를 사용하지 않는다. 대신 .yarn 경로 하위에 의존성들을 .zip 포맷으로 압축 저장하고, .pnp.cjs 파일을 생성 후 의존성 트리 정보를 단일 파일에 저장한다. 이를 인터페이스 링커 (Interface Linker) 라고 한다.

Untitled

.yarn/cache 폴더에 의존성의 정보가 저장되고, .pnp.cjs 파일에 의존성을 찾을 수 있는 정보가 기록되기 때문에 이를 통해 디스크 I/O 없이 어떤 패키지가 어떤 라이브러리에 의존하는지, 각 라이브리는 어디에 위치하는지 바로 알 수 있다.

Untitled

.pnp.loader.mjs 를 통해 .pnp.cjs 를 가져오게 된다.

.pnp.cjs

/* empotion/styled 패키지 중에서*/ 
["@emotion/styled", [
		/* npm: 10.3.0 버전은 */
        ["npm:10.3.0", {
					/* 이 위치에 있고 */
          "packageLocation": "./.yarn/cache/@emotion-styled-npm-10.3.0-65b17d7921-9d9609c008.zip/node_modules/@emotion/styled/",
          /*이 의존성을 참조한다 */
					"packageDependencies": [
            ["@emotion/styled", "npm:10.3.0"]
          ],
          "linkType": "SOFT"
        }],

이런 식으로 패키지의 위치와 의존성 목록을 완전하게 기록하기 때문에 특정 패키지와 의존성에 대한 정보가 필요할 때 바로 알 수 있다.

이렇게 링커를 사용함으로써 패키지를 검색하기 위한 비효율적이고 반복적인 디스크 I/O로부터 벗어날 수 있게 되었다. 또한 유령 의존성 문제도 해결 가능해졌다.

위와 같이 .pnp.cjs는 의존성 트리를 중첩된 맵으로 표현했다. 기존 Node가 파일 시스템에 접근해 직접 I/O를 실행하던 require 문의 비효율을 자료구조를 메모리에 올리는 방식으로 탐색을 최적화한다.

zero-installs

.yarn 폴더에 받아놓은 파일들은 캐시 역할을 한다.

커밋에 포함시켜 github에 프로젝트 코드와 함께 올려두면 어디서든 같은 환경에서 실행 가능할 것을 보장할 수 있으며 별도의 설치 과정도 필요가 없다.(예외 케이스도 존재)

만일 의존성에 변경이 발생하더라도 git 상에서 diff로 잡히기 때문에 쉽게 파악이 가능하다. 따라서 installd을 할 필요가 없고, 개발자들 간 node_modules가 동일한 지 체크할 필요가 없다.

만약 로컬에 설치된 파일과 리모트에 설치된 파일이 위치 등이 달라 디버깅을 어렵게 한다면 대응하기 매우 어려워질 것이다.

Zero Install을 사용한다면 설치환경에서든 같은 상황임을 명식적으로 보장할 수 있다.

부가적인 장점으로 현재 브랜치에 맞는 package.json에 맞게 node_modules를 갱신하기 위한 반복적인 yarn install을 할 필요 또한 없다.
브랜치를 체크아웃할 때 마다 .yanr/cache 폴더에 있는 의존성도 커밋으로 잡혀있어 여타 파일들처럼 파일로 취급되어 함께 변경된다.

도입 방법

$ npm install -g yarn
$ cd ../path/to/some-package
$ yarn set version berry

// yarn -v 을 했을 때 v3이 나와야 함

yarn berry는 기존 Node.js 의존성 관리 시스템과 많이 다르기 때문에 하위호환을 위해 패키지 단위로만 도입이 가능하다.

nodeLinker : pnp 로 해야 zero-install 이 가능하다

nodeLinker: pnp

yarnPath: .yarn/releases/yarn-3.6.1.cjs

packageExtensions:
  'styled-components@*':
    dependencies:
      'react-is': '^16.8.0'

단점

1. cache 파일의 크기

pnpm의 .cache파일은 굉장히 크다. node_modules 대신 패키지들을 압축해 관리하고, 그것을저장소에 같이 올리기 때문에 패키지 설치 시간을 없애고 모두가 같은 환경을 공유한다는 장점이 있지만. 이를 올리기 위해 Git 커밋 크기를 조정해야 하고, 대용량 파일 저장소도 사용할 필요도 있다. 이 때문에 사용하는데 불편했다. 또 .git 디렉토리 자체가 무거워진다.
https://engineering.ab180.co/stories/yarn-to-pnpm 이 글에서도 나와잇듯, git에 부하를 줄 수 있다.

2. 패키지 호환

PnP 형태에 맞게 호환이 되어있지 않은 패키지들이 아직 많다.

3. 러닝커브

해야 하는 설정들이 많고, 이 때문에 대규모, 팀 프로젝트 프로젝트일수록 잘못 적용하거나, 오래 걸리는 경우나, 잘못 사용하고 있는 경우가 많다.

(2편에 계속...)

profile
코드는 죄가 없다,,

0개의 댓글

관련 채용 정보