노드 모듈들은 기본적으로 시멘틱 버저닝이라는 기법을 사용하여 사용하는 모듈들의 버전을 나타낼 것을 권장하고 있습니다. 시멘틱 버저닝이란 간단히 말해 1.2.3
처럼 버전을 세 가지 숫자가 들어갈 수 있는 자리로 구분하고, 각각의 자리에 현재 버전이 이전 버전과 어떤 관계가 있는지 암시하도록 하는 방법입니다. 예를 들어 첫 번째 자리에 들어가는 숫자가 다르면, 두 버전은 이전 버전과 호환되지 않는 완전히 새로운, 메이저 버전이라는 의미입니다.
위 사진을 보면 캐럿 기호(^)
가 들어가 있는데, 이는 가장 앞 숫자인 메이저 버전을 제외한 두 자리 (마이너, 패치) 버전까지는 변경을 허용할 수 있다는 의미입니다. 현재 사진 속 바벨 코어의 버전이 ^7.17.9
이므로, 메이저 버전인 7이 변경되지 않는 범위에서는 모듈의 버전이 바뀌는 것을 허용합니다. 7.17.10
버전이나, 7.18.0
버전 등이 모두 사용될 수 있다고 볼 수 있습니다.
이때 npm install
명령어를 통해 모듈들을 설치하면 해당 메이저 버전 중 최신 버전을 다운받게 됩니다. 빌드에 따라 유연하게 버전을 선택할 수 있다는 장점도 있지만, 이럴 경우 사용하는 모듈간의 버전 불일치로 인해 문제가 발생할 수도 있습니다. 서로 다른 환경에서 하나의 프로젝트를 개발 한다는건 어떤 문제가 발생할지 모르는 잠재적인 위험을 안고 가는 것과 같습니다.
개발자의 환경에 따라 모듈들의 설치 순서가 변경됩니다.
만들어진 프로젝트에서 모듈을 추가로 설치하게 될 경우, 그 뒤에 npm i
명령어를 통해 처음부터 설치하는 사람과 모듈 설치 순서가 달라질 수 있습니다. npm은 모듈 이름을 사전 순서대로 정렬하여 순차적으로 설치하기 때문입니다. 서로 다른 환경이 만들어지기 때문에 좋은 징조라고 보기 어렵습니다.
npm은 모듈들을 한 번에 하나씩만 순차적으로 설치합니다. 설치해야하는 모듈이 많으면 많을수록, 총 설치 시간이 길어지게 됩니다. 첫 모듈 설치 시간이 길어지면 빌드 및 배포 시간에도 부정적인 영향을 끼치게 됩니다.
2016년 10월 출시
npm 문제들을 해결함과 동시에, 여러가지 기능들을 함께 탑재하여 등장
yarn은 사용할 모듈의 버전을 지정하기 위해 프로젝트에 .lock
파일을 포함합니다. 정확한 버전을 지정하고 고정하기 때문에 다른 사용자가 프로젝트를 개발할때 항상 같은 버전의 모듈을 사용할 수 있도록 보장합니다. 이 파일 덕분에 기존 npm의 일관적이지 않은 패키지 버전 문제를 해결할 수 있었습니다.
물론 이 시점의 npm 메인테이너들도 이러한 문제를 인지하고 있었고, 패키지의 버전을 고정시키는 방법도 제시했었습니다. npm shrinkwrap
명령어를 사용하면 .lock
파일과 유사하게 패키지들의 버전을 포함한 npm-shrinkwrap.json
파일을 생성해줍니다. 이 파일이 프로젝트에 함께 있을경우, package.json
에 명시된 버전을 무시하고 json 파일에 적힌 버전을 설치하게 됩니다.
하지만 npm의 경우 고정이 필요할때마다 매번 명령어를 입력하여 json 파일을 생성하는 과정이 필요했습니다. yarn
은 이러한 버전 고정 파일을 자동으로 생성합니다.
yarn은 패키지가 제대로 설치되었는지 확인하기 위해 checksum을 사용합니다. yarn.lock
파일을 확인해보면 resolved
주소 뒤에 해시값이 추가 되어있는 것을 확인할 수 있습니다. 이 해시값이 바로 checksum입니다.
물론 중간에 설치 요청을 가로채거나 원본을 수정하는등 공격자가 비집고 들어 올 확률은 낮지만, 제대로 설치되지 않은 경우를 대비해 패키지 파일의 무결성을 확인하는 안전장치가 추가된 것은 긍정적인 일입니다.
yarn의 장점에서 빠질 수 없는 요소는 역시 속도입니다. yarn은 캐시를 사용하여 한 번 다운로드 한 패키지라면 그 다음부터는 빠른 속도로 설치할 수 있습니다. 인터넷이 연결되지 않은 환경에서도 설치가 가능합니다. 또한 병렬 다운로드를 지원하여 순차적으로 설치해야하는 npm과 달리 모듈들을 한꺼번에 설치해버립니다. 덕분에 npm보다 속도가 빠릅니다.
npm보다 빠른 속도에 대한 yarn의 답변
" Is Yarn faster than other package managers?
Shrug 🤷♀️
At the time Yarn got released Yarn was effectively much faster than some of its competitors. Unfortunately, we failed to highlight that performance wasn't the main reason why we kept working on Yarn. Performances come and go, so while we were super fast it wasn't so much because we were doing something incredibly well, but rather that the competing implementations had a serious bug. When that bug got fixed, our miscommunication became more apparent as some people thought that Yarn was all about performances. "
시간이 지나고, npm도 버전이 올라가면서 많은 부분이 개선되었습니다. 현재는 버전 고정을 위한 package-lock
파일도 자동으로 추가되고, 속도면에서도 yarn과 큰 차이가 없는 수준까지 따라왔습니다. node_modules
또한 이제 결정적(deterministic)인 트리 구조로 만들 수 있게 되었습니다.
yarn 또한 시간이 지나며 많이 발전하였고, 사용자가 많아지고 기능들이 추가되며 yarn 생태계 자체도 차츰 안정되어 갔습니다.
그러나 여러 발전을 거친 두 패키지 매니저 프로그램들에게도 여전히 남아있는 문제점들이 있었습니다.
npm의 node_modules
폴더는 큰 용량을 자랑하기로 유명합니다.
수 많은 패키지들과 그 패키지가 의존하는 모듈들을 전부 설치해버린다면 무거운 node_modules 폴더가 탄생해버리겠죠. npm은 이 무거운 폴더를 경량화시키기 위해 호이스팅을 도입하고 있습니다. npm은 속도 문제를 개선하기 위해 호이스팅 등 최적화 알고리즘을 도입하였으나 부작용으로 유령 의존성
이라는 문제를 새로 낳고 말았습니다.
용량을 줄이는 가장 간단한 방법은 중복을 제거하는 것입니다. npm과 yarn은 node_modules 내부의 중복된 패키지를 최소화하기 위해 각 패키지가 의존하고 있는 패키지들을 최상단으로 끌어올려 버립니다. 이렇게 되면 프로젝트가 의존하고 있는 패키지의 내부에 존재하던 각각의 node_modules에 있는 중복된 패키지가 최상단에 하나만 존재하게 되어 불필요한 중복을 제거할 수 있게 됩니다.
하지만 직접 설치하지 않고, 간접 설치한 종속성에 개발자가 접근할 수 있게 되는 상황이 벌어지기도 합니다. 존재하지 않는 종속성에 의존하는 코드가 발생할 수 있다는 뜻입니다. 이를 유령 의존성
이라고 합니다.
내가 설치한 패키지가 의존하고 있다는 이유로 그 패키지까지 슬쩍 불러올 수 있게 됩니다. 마치 유령처럼 나는 설치한 적이 없지만, 그 패키지는 최상단에 존재 하고 있습니다. 이러한 현상을 유령 의존성이라고 부릅니다.
yarn berry에서는 이런 식의 호이스팅 동작이 일어나지 않도록 nohoist
옵션이 기본적으로 활성화 되어 있습니다.
🗣️ 저는 실제로 회사 프로젝트를 진행하는 도중에 npm에서 yarn을 설치해서 사용하다가 의존성 문제로 start가 안된적이 있습니다...ㅎ...
이 외에도 비효율적인 설치와 의존성 검색 등은 여전히 npm과 yarn에게 숙제로 남아있었습니다.
node_modules구조 하에서 모듈을 검색하는 방식은 기본적으로 디스크 I/O 작업입니다. 이는 node_modules가 가진 문제이기 때문에 yarn classic과 npm 모두에 해당되는 내용입니다.
개발자가 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 라는 기술을 통해 이를 개선합니다.
2020년 1월 25일 출시
yarn v2 이상의 modern version yarn을 이르는 명칭입니다.
기존의 yarn v1은 yarn classic
이라고 부르게 되었습니다.
Plug'n'Play는 yarn berry가 제공하는 새로운 패키지 관리 시스템입니다.
기존의 무거웠던 node_modules 대신, 패키지들에 대한 정보는 .zip
파일로 압축하여 .yarn/cache
폴더에 저장하고 이를 찾기 위한 정보를 .pnp.cjs
파일에 생성 후 의존성 트리 정보를 단일 파일에 저장합니다. 이를 인터페이스 링커 (Interface Linker)
라고 합니다.
아래 코드는 pnp.cjs의 일부입니다
["@babel/helper-module-transforms", [\
["npm:7.19.6", {\
"packageLocation": "./.yarn/cache/@babel-helper-module-transforms-npm-7.19.6-c73ab63519-c28692b37d.zip/node_modules/@babel/helper-module-transforms/",\
"packageDependencies": [\
["@babel/helper-module-transforms", "npm:7.19.6"],\
["@babel/helper-environment-visitor", "npm:7.18.9"],\
["@babel/helper-module-imports", "npm:7.18.6"],\
["@babel/helper-simple-access", "npm:7.19.4"],\
["@babel/helper-split-export-declaration", "npm:7.18.6"],\
["@babel/helper-validator-identifier", "npm:7.19.1"],\
["@babel/template", "npm:7.18.10"],\
["@babel/traverse", "npm:7.19.6"],\
["@babel/types", "npm:7.20.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@babel/helper-optimise-call-expression", [\
["npm:7.18.6", {\
"packageLocation": "./.yarn/cache/@babel-helper-optimise-call-expression-npm-7.18.6-65705387c4-e518fe8418.zip/node_modules/@babel/helper-optimise-call-expression/",\
"packageDependencies": [\
["@babel/helper-optimise-call-expression", "npm:7.18.6"],\
["@babel/types", "npm:7.20.2"]\
],\
"linkType": "HARD"\
}]\
]],\
위와 같이 .pnp.cjs는 의존성 트리를 중첩된 맵으로 표현하였습니다. 기존 Node 가 파일시스템에 접근하여 직접 I/O 를 실행하던 require문의 비효율을 자료구조를 메모리에 올리는 방식으로 탐색을 최적화한 것입니다. 의존성 압축을 통하여 디스크 용량 절감 효과도 볼 수 있습니다. du -sh
명령어로 확인해보았을 때, Next.js 기반 어드민 서비스 기준 913MB → 247MB
로 기존 패키지 용량 대비 약 27%
수준으로 패키지 관련 용량이 감소한 것을 확인할 수 있습니다.
다만 .yarnrc.yml의 링커 설정을 pnp가 아닌 node-modules
로 하게 된다면 기존처럼 node_modules를 설치하여 의존성을 관리하게 됩니다. 하지만 이렇게 사용할 경우 앞서 설명드린 PnP의 장점들을 활용하지 못하게 됩니다.
이에 대한 예시로 최근 Vercel
에서 모노레포 툴링으로 발표한 Turborepo
의 경우 패키지 매니저 중 pnpm
의 pnp 모드만 지원하고 있고, 메인테이너는 yarn berry의 경우 지원 계획을 취소한 상태입니다. 이 경우 앞서 말씀드린 방식으로 berry를 사용해야 합니다. 관련 이슈는 여기에서 확인하실 수 있습니다.
즉, 별도의 I/O 작업 없이도 패키지의 위치를 정확히 알 수 있기 때문에 시간도 단축되고, 중복 설치를 방지하며, node_modules를 만들고 패키지들을 호이스팅시킬 필요가 없는 Plug'n'Play의 특성 덕분에 유령 의존성 문제도 해결할 수 있었습니다. 패키지를 압축한 덕분에 용량도 줄었습니다.
Plug'n'Play 전략으로 무거웠던 node_modules를 획기적으로 제거하고 옮긴 덕분에 이를 이용해 의존성까지 github에 올릴 수 있게 되었습니다.
github는 파일당 최대 용량을 500mb으로 제한하고, 원활한 이용을 위해 저장소당 1gb 미만의 크기를 유지 할 것을 권장하고 있습니다.
yarn berry를 통해 만든 의존성 폴더는 어지간히 크지 않은 이상 200mb를 넘지 않습니다. 덕분에 git clone
이후 별도의 설치가 필요 없이, 바로 사용할 수 있도록하는 zero-install을 시도해 볼 수 있습니다.
zero-install을 사용할 경우 로컬에서의 귀찮음도 줄어들지만, CI/CD 파이프라인을 구축한 경우 더 큰 효과를 볼 수 있습니다. 클론이 끝나자마자 곧바로 빌드가 가능해진 덕분에 배포까지 걸리는 속도가 대폭 단축됩니다.
🔗참고링크
https://usage.tistory.com/147
https://toss.tech/article/node-modules-and-yarn-berry
https://blog.dramancompany.com/2023/02/리멤버-웹-서비스-좌충우돌-yarn-berry-도입기/
잘 보고 갑니다! 큰 도움이 됐어요