모던한 환경에서 자바스크립트를 사용하는 개발자라면 흔히 npm을 사용해보았을 것이다. 자바스크립트 패키지들을 한 프로젝트에서 사용할 수 있도록, 그리고 누구나 배포할 수 있도록 만들어진 패키지 매니저가 바로 npm이다. node.js에 기본으로 내장된 패키지매니저인 만큼 점유율이 가장 높기도 하다. 그러나 사용하다보면 여간 불편한 점들이 존재하기 마련이다. 특히 package.json을 통해 생성하는 node_modules는 관리하기가 상당히 까다롭다. 본 포스팅에서는 npm의 문제점을 짚어보고, 대안으로 등장한 yarn berry의 개념 및 간단한 사용법을 알아보고자 한다.
npm의 문제점
npm으로 만들어진 프로젝트에는 크게 package.json, package-lock.json, 그리고 거대한 node_modules 디렉토리가 존재한다. 바로 여기서 비효율적인 지점을 발견할 수 있다.
너무 무거운 node_modules
npm에서 구성하는 node_modules는 상당히 큰 디스크 공간을 차지한다. 간단한 cli 프로젝트라 할지라도 수백 메가바이트의 node_modules를 필요로 하게 된다. 용량을 많이 차지하기도 할 뿐더러, 패키지를 탐색하는 데에도 많은 I/O 작업이 들어가게 된다.
비효율적인 모듈 탐색 과정
파일 시스템을 활용하는 node_modules 구조에서 필요한 모듈을 검색하는 방식은 기본적으로 디스크 I/O 작업이다. node.js가 모듈을 불러올 때 경로 탐색에 사용되는 규칙은 공식 문서에서도 확인할 수 있다. 다음은 require.resolve.paths 명령어를 통해 npm 프로젝트에 설치된 react를 찾는 과정이다.
이처럼 매 탐색마다 수 많은 폴더와 파일을 열고 닫으면서 검색하게 된다. 이렇게 모듈 탐색을 메모리상에서 자료구조로 처리하지 않고, I/O로 직접 처리하다 보니 최적화도 어렵고, 경로도 복잡하다. 또한 패키지를 찾지 못하면 상위 디렉토리의 node_modules 폴더를 계속 탐색하다 보니, 상위 디렉토리가 어떤 node_modules를 가지고 있는지에 따라 의존성을 불러올 수 있기도 하고, 없기도 하는 일도 발생하게 된다. 환경에 따라 동작이 달라진다는 점은 상당히 크리티컬하다.
패키지 install..언제 끝나지?
npm 프로젝트에서 개발환경을 설정할 때, 우리는 package.json에 명시된 필요한 패키지들을 설치해야 한다. 이때 모든 패키지 설치를 순차적으로 하기 때문에 다운로드 속도가 매우 느릴 수밖에 없다. 필자가 상반기에 작업하던 레거시 프로젝트의 경우, npm install에만 5분 이상이 걸린 적도 허다하다.
유령처럼 돌아다니는 의존성
npm은 node_modules에 필요한 의존성 패키지가 중복해서 설치되는 것을 막기 위해, 일종의 최적화로 호이스팅 기법을 사용한다. 만약 의존성 트리가 왼쪽의 모습을 하고 있다면, 패키지 A와 패키지 B는 두 번 설치되므로 디스크 공간을 낭비하게 된다. npm에서는 이를 방지하기 위해 의존성 트리의 모양을 오른쪽 그림처럼 사용한다. 패키지 A에 의해 간접 설치되는 패키지 B는 이제 직접 의존성마냥, require문을 통해 개발자가 직접 접근할 수 있게 된다. 존재하지 않아야 할 종속성에 의존하는 코드가 생길 수도 있다는 말이다.
실제 npm으로 생성한 프로젝트에 react 의존성을 하나 추가해본 결과, 다음과 같이 react에 종속된 loose-envify 패키지가 node_modules의 최상단에 위치하는 것을 확인해볼 수 있다.
yarn berry의 등장
2016년, 페이스북은 npm이 가지고 있던 보안, 성능 문제 등을 해결하기 위해 새로운 패키지 매니저 yarn(Yet Another Resource Negotiator)을 발표했다. 이는 다음과 같은 장점을 가지고 있었다.
- 병렬화를 통한 패키지 다운로드 속도 개선
- 자동화된 lock 생성
- 의존성 트리가 덜 변형되는 알고리즘 변경
- 캐시 파일을 통한 다운로드 속도 개선
그러나 npm과 동일하게 node_modules 파일시스템에 의존하는 의존성 관리 시스템은 동일했고, 결국 지금의 yarn berry 라고 불리는 yarn2가 등장하게 된다. (yarn v1는 현재 레거시 프로젝트로 간주되어 더 이상의 유지보수를 하지 않는다고 한다. v1는 yarn classic이라는 네이밍으로 불리고 있다.)
yarn berry는 Plug’n’Plan(PnP) 라는 기술을 도입하여 npm과 yarn v1이 가지고 있던 문제들을 해결했다. 패키지 매니저가 node_modules를 만드는 것에 그치지 않고, 보다 근본적으로 안전하게 의존성을 관리하고자 등장했다.
Plug’n’Plan(PnP)
yarn berry에서는 더 이상 node_modules를 사용하지 않는다. 대신 모듈들을 zip 파일로 관리하여 .yarn/cache 폴더에 두고, 해당 모듈들을 .pnp.cjs, .pnp.loader.mjs 를 통해서 가져오게 된다. 다음과 같이 yarn berry로 프로젝트를 세팅해보자.
프로젝트 초기화
# yarn install
npm install yarn
# make some directory
mkdir yarn-project
cd yarn-project
# initialize project
yarn init
# set yarn version to yarn berry
yarn set version berry
위 과정을 거치면 package.json이 포함된 프로젝트를 생성할 수 있다.
디렉토리 구조
여기서 yarn install로 react 의존성을 추가해본 결과는 다음과 같다.
기존 npm에서 사용하던 node_modules가 존재하지 않는 것을 확인할 수 있다. 대신, .yarn/cache 폴더에 react와 의존성 패키지 정보가 zip 포맷으로 압축 저장된다. 패키지 설치 시간이 줄어들고, 의존성 압축을 통하여 디스크 용량도 절감된다.
이때 .pnp.cjs 파일에 의존성을 찾을 수 있는 트리 정보가 기록된다. 얘를 인터페이스 링커(Interface Linker) 라고 한다. 링커를 사용함으로써 패키지를 탐색하기 위한 비효율적인 디스크 I/O 과정으로부터 벗어날 수 있게 되었다. 또한 의존성을 쉽게 검증할 수 있게 됨으로써 유령 의존성 문제도 해결될 수 있게 되었다. 다음 코드는 .pnp.cjs 파일의 일부다.
["react", [\
["npm:18.2.0", {\
// npm 18.2.0 버전이 위치한 경로
"packageLocation": "./.yarn/cache/react-npm-18.2.0-1eae08fee2-88e38092da.zip/node_modules/react/",\\
// 참조하는 의존성 목록
"packageDependencies": [\
["react", "npm:18.2.0"],\
["loose-envify", "npm:1.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
이 .pnp.cjs 파일이 제공하는 자료구조를 통해 의존성을 검색할 때 node_modules를 순회할 필요도 없게 되었다. 또한 모든 의존성이 .pnp.cjs 파일을 통해 관리되기 때문에 외부 환경에 영향을 받지 않게 되었다. 이로서 개발환경에 따라 패키지 참조가 다르게 될 수 있는 문제도 사라졌다.
Zero-Installs
위에서 언급한 장점들에 얹어, yarn berry에서는 의존성을 Git 등 버전 관리에 포함시키는 것도 가능해진다. 기존 npm의 node_modules는 파일의 크기가 거대하고 개수도 방대했던 반면, yarn berry의 경우 의존성을 압축 파일인 zip으로 관리하기 때문에 용량이 작다. 따라서 의존성 자체를 버전 관리에 포함시키는 것도 가능하며, 이를 zero-install이라고 부른다. 의존성을 커밋에 포함시킨다면 어디서든 같은 환경에서 실행 가능할 것을 보장하며, 별도의 설치 과정도 불필요하다. 로컬에 설치된 파일과 리모트(CI 환경, 동료 개발자들의 개발환경 등)에 설치된 파일이 같다는 것을 보장할 수 있다면 디버깅에도 매우 용이해질 것이다.
결론
yarn berry는 최적화, DX, 보안 등 여러 방면에서 확실한 이점을 가진 패키지매니저다. 기존에 npm, yarn classic을 사용하고 있다면 한번쯤 마이그레이션을 생각해볼 만한 좋은 도구임에 틀림없다. 물론 pnpm 등 빠르고 효율적인 디스크 관리 기법을 가진 다른 패키지 매니저도 존재한다. 또한 패키지 매니저의 선구자인 npm은 여전히 압도적인 점유율과 커뮤니티를 보유하고 있다. 각각의 장단점을 파악하고, 프로젝트의 상황에 맞게 최적의 패키지 매니저를 선택하면 된다.
References
'Web > Node.js' 카테고리의 다른 글
디펜던시가 latest로 깔렸을 때 (0) | 2023.10.01 |
---|---|
husky & lint-staged로 린트 검사 자동화하기 (2) | 2023.06.11 |
[npm] package-lock.json이 필요한 이유 (0) | 2023.03.12 |