node_modules의 문제점과 yarn berry의 특징

Imnottired·2023년 6월 8일
0

yarn berry에 관해 글을 쓰려고한다.
기존의 yarn (v1 or yarn classic)이라고 하고,
yarn v2부터 yarn berry라고 하는데, 작동하는 방법 자체가 많이 바뀌어서
효율을 극대화하였다.
그래서 오늘은 yarn berry의 특징과 node_modules을 사용하는 것에 대한 문제점을 다뤄보고자 한다.




node_modules의 문제점

npm 은 그간 Node 생태계를 위해 많은 일을 해왔지만 가장 첫 번째로 꼽힐 용량 문제를 제외하고서라도 많은 문제를 안고 있었다. 아래는 yarn 공식 문서에 언급되어 있는 내용이다.

1) 모듈 탐색 과정의 비효율
node_modules 구조 하에서 모듈을 검색하는 방식은 기본적으로 디스크 I/O 작업입니다. 이는 node_modules가 가진 문제이기 때문에 yarn classic과 npm 모두에 해당되는 내용입니다.

디스크 I/O 작업 : 디스크로부터 파일을 읽거나 디스크에 파일을 쓰는 것은 디스크 I/O 작업입니다. 데이터베이스에서 레코드를 검색하거나 업데이트하는 작업도 디스크 I/O 작업을 수반합니다.

한편, 디스크 I/O 작업은 디스크 액세스 속도가 상대적으로 느리기 때문에, 시스템 성능에 영향을 줄 수 있습니다. 따라서 디스크 I/O 성능을 향상시키기 위한 최적화 작업이 중요합니

개발자가 node_modules 내부에서 특정 라이브러리를 불러오는 상황을 가정해보겠습니다. Node.js가 모듈을 불러올 때 경로 탐색에 사용하는 몇 가지 규칙이 있다.
이 규칙은 Node.js 공식 문서에서 확인할 수 있습니다.

require()의 경우 1) fs, http 등의 코어 모듈이 아니면서, 2) 절대 경로를 사용할 경우 대략 아래와 같은 순서로 순회하며 모듈을 검색합니다.

다음은 '/home/ry/projects/foo.js' 에서 require('bar.js') 를 탐색할 경우입니다.

/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

이처럼 매 탐색마다 수 많은 폴더와 파일을 실제로 열고 닫으면서 검색할 수 밖에 없으며, node_modules 중첩 등 경우에 따라서는 순회해야 하는 경로가 이보다 복잡해질 수 있습니다.

패키지 설치 과정의 경우에도 마찬가지 입니다. 설치 과정에 필요한 최소 동작만으로도 이미 비용이 많이 들고 있기 때문에 각 패키지 간 의존 관계가 유효한지 등의 추가적인 검증에 리소스를 할당하기 어렵습니다.

이처럼 모듈 탐색을 메모리 상에서 자료구조로 처리하지 않고 I/O로 직접 처리하다보니 추가적인 최적화가 어렵습니다. 실제로 yarn 개발진은 이러한 이유들로 더 이상 최적화 할 여지가 없었다고 문서에서 밝히고 있습니다. yarn berry에서는 이 뒤에서 언급될 PnP 라는 기술을 통해 이를 개선합니다.


2) 유령 의존성 (Phantom Dependency)
물론 npm은 속도 문제를 개선하기 위해 호이스팅 등 최적화 알고리즘을 도입하였으나 부작용으로 유령 의존성 이라는 문제를 새로 낳고 말았습니다.


npm, yarn classic 등은 중복 설치를 방지하기 위해 위 그림처럼 종속성 트리 아래에 존재하는 패키지들을 호이스팅 & 병합합니다. 그렇게 하면 패키지 최상위에서 트리 깊이 탐색하지 않고 루트 경로에서 원하는 패키지를 탐색할 수 있으므로 효율적입니다.

하지만 이런 효율의 반대 급부로는 직접 설치하지 않고, 간접 설치한 종속성에 개발자가 접근할 수 있게 되는 상황이 벌어지기도 합니다. 존재하지 않는 종속성에 의존하는 코드가 발생할 수 있다는 뜻입니다.

이를 유령 의존성 이라고 합니다. 앞서 언급한 node_modules의 단점으로 인해 의존성 트리의 유효성을 검증하기 어렵다는 것도 한 몫을 했습니다.

yarn berry에서는 이런 식의 호이스팅 동작이 일어나지 않도록 nohoist 옵션이 기본적으로 활성화 되어 있습니다.

yarn Berry의 특징

Zero Install
Zero Install은 말 그대로 설치를 하지 않고 이용하는 방식을 말한다.

기존에 node_modules에서 모든 디펜던시를 새로 인스톨하려고 하면, 모든 디펜던시 모듈이 인스톨에만 굉장히 많은 시간이 소모하게 된다.

그러나 Zero Install을 이용하면 디펜던시 Install에 걸리는 시간을 없앨 수 있기 때문에, CI 단이나 배포 파이프라인 등에서 반복되는 install 시간을 단축 시킬 수 있게 되고 CI 실행시간 및 배포 시간을 대폭 감소 시킬 수 있다.

그럼 이러한 Zero Install 방식을 어떤 방식으로 적용해 볼 수 있을까?
Yarn에서는 Yarn Berry 버전을 통해 PnP(Plug And Play)라는 기능을 지원하는데, 이 기능을 이용해서 zero install을 할 수 있다.

.yarn 폴더에 받아놓은 파일들은 오프라인 캐시 역할 또한 할 수 있습니다. 커밋에 포함시켜 github에 프로젝트 코드와 함께 올려두면 어디서든 같은 환경에서 실행 가능할 것을 보장할 수 있으며 별도의 설치 과정도 필요가 없습니다.

만약 의존성에 변경이 발생하더라도 git 상에서 diff로 잡히므로 쉽게 파악 가능합니다. 개발자들 간 node_modules가 동일한지 체크할 필요가 없다는 뜻입니다.

제가 생각했을 때 Yarn berry 도입 시 가장 강조되어야 할 중요한 지점이라고 생각합니다. 우리가 작성한 코드들이 여러 툴체인을 거치는 동안 많은 파일들이 generate 되는데, 만약 로컬에 설치된 파일과 리모트(CI 환경, 실서비스 등)에 설치된 파일이 달라 디버깅을 어렵게 한다면 대응하기 매우 어려워질 것입니다. Zero Install을 사용하게 된다면 어떤 설치 환경에서든 같은 상황임을 명시적으로 보장할 수 있습니다.

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

PnP(Plug And Play)
그럼 pnp에 대해서 한번 알아보자.

https://yarnpkg.com/features/pnp
https://classic.yarnpkg.com/lang/en/docs/pnp/
https://github.com/yarnpkg/berry/issues/850
yarn berry는 Plug’n’Play(PnP) 라는 기술을 사용하여 이러한 문제들을 해결합니다. yarn berry는 node_modules를 사용하지 않습니다. 대신 .yarn 경로 하위에 의존성들을 .zip 포맷으로 압축 저장하고, .pnp.cjs 파일을 생성 후 의존성 트리 정보를 단일 파일에 저장합니다. 이를 인터페이스 링커 (Interface Linker) 라고 합니다.

https://yarnpkg.com/api/interfaces/yarnpkg_core.linker.html
링커를 논리적 종속성 트리와 파일 시스템 사이에 있는 일종의 접착제로도 비유할 수 있습니다. 이러한 링커를 사용함으로서 패키지를 검색하기 위한 비효율적이고 반복적인 디스크 I/O로부터 벗어날 수 있게 되었습니다. 의존성 또한 쉽게 검증할 수 있어 유령 의존성 문제도 해결 가능해졌습니다.

아래 코드는 pnp.cjs의 일부입니다
위와 같이 .pnp.cjs는 의존성 트리를 중첩된 맵으로 표현하였습니다. 기존 Node 가 파일시스템에 접근하여 직접 I/O 를 실행하던 require 문의 비효율을 자료구조를 메모리에 올리는 방식으로 탐색을 최적화한 것입니다. 의존성 압축을 통하여 디스크 용량 절감 효과도 볼 수 있습니다. du -sh 명령어로 확인해보았을 때, Next.js 기반 어드민 서비스 기준 913MB → 247MB 로 기존 패키지 용량 대비 약 27% 수준으로 패키지 관련 용량이 감소한 것을 확인할 수 있습니다.


마무리

yarn berry를 직접 사용하고 그것을 사용한 이유에 대해 적어보았다.
직접 사용한 것 까지 적으면 글이 무거워질 것 같아서 사용한 이유에 대해서만 적어보았고 다음 글에서는 실제 적용한 것을 올리겠다.

profile
새로운 것을 배우는 것보다 정리하는 것이 중요하다.

0개의 댓글