테슬라 클론 코딩 영상을 따라하던 중, styled-components 설치에서 문제가 생겼었다. 강사의 명령어를 따라 입력하면 yarn을 통해 설치하거나 npm으로 설치할 때 latest 버전으로 설치하라는 문구가 떴고, 후자의 방법을 통해 오류 메시지는 없앴지만 더 많은 문제들을 불러오기 시작했다.
이후 설치하는 모든 라이브러리, 모듈 등에서 upstream dependency conflict가 발생하기 시작했다.
Could not resolve dependency: npm ERR! peer styled-components@"^5.3.1" from @mui/styled-engine-sc@5.12.0
위 오류 메시지는 MUI를 설치하려 할 때 발생한 오류로, MUI 공식 문서에서 권장하는 styled-components 버전이 5.3.1인데, latest 옵션을 통해 설치된 styled-components 버전이 이와 상충되며 벌어진 문제였다.
다른 라이브러리 설치는 force 옵션을 통해 77개의 vulnerability와 함께 밀고 나갔으나 MUI는 끝까지 설치되지 않았다.
혹시 내가 yarn을 통해 설치했다면 상황이 조금 달라졌을까하는 생각에 npm과 yarn에 대해 찾아보게 되었다. (해당 문제는 npm 자체가 peer-dependency를 가지고 있어 발생하는 문제였다.)
npm과 yarn이 무엇인지는 npm이 무엇의 약자인지를 찾아보면 쉽게 알 수 있다. npm이란 Node Pakage Manager로, Node.js의 패키지 매니저를 의미한다. yarn은 페이스북에서 개발한 패키지 매니저이다. 개발자들이 npm 온라인 데이터 베이스에 업로드한 패키지들은 이러한 패키지 매니저 도구를 통해 설치, 삭제될 수 있다.
패키지 매니저 도구가 없던 시절에는 프로젝트에 원하는 라이브러리를 추가하기 위해선 깃허브에서 직접 소스를 다운받아 프로젝트에 넣어야 했었다. 패키지 매니저 도구가 등장하면서 이러한 귀찮음이 해결되었다. 즉, 패키지 매니저 도구 덕분에 js를 기반으로 만들어진 모듈을 쉽게 다운받고 프로젝트에 추가하거나 관리하는 것이 가능해진 것이다.
속도, 안정성, 보안성이 yarn이 탄생하게 된 가장 주된 이유이다.
기존 npm의 방식은 순차적인 설치를 진행하였으나 yarn은 패키지의 병렬 설치를 가능하게 하여 속도의 개선을 이루어냈다. 또한, 캐시를 사용하여 중복된 데이터를 다시 다운받지 않도록 하여 속도를 개선하기도 했다.
yarn은 패키지 설치 시 yarn.lock을 자동으로 설치한다. 이 yarn.lock 파일은 모든 디바이스에 같은 패키지가 설치되도록 보장하기 때문에 버전 차이로 인한 버그를 방지할 수 있다.
패키지 매니저 도구를 사용하면 package.json 파일이 생기고, 해당 파일은 프로젝트에서 사용하는 패키지의 이름과 버전을 관리하는 데에 사용된다. 개발자가 install 커멘드 (npm install or yarn install)를 사용하면 package.json 파일에 저장되어 있는 모든 패키지가 다운받아져 프로젝트에 직접적 혹은 간접적으로 필요한 패키지들이 node_modules에 담긴다.
package.json 파일에서 패키지 버전을 명시할 때 ~이나 ^를 사용하면 설치시기에 따라 개발자마다 상이한 패키지 버전을 가지기 쉽다. 서로 다른 버전의 패키지를 사용한다는 것은 특정 PC나 서버에서 동작할지 여부가 달라질 수 있기 때문에 지양하는 것이 좋다.
이 문제를 해결하기 위해 yarn.lock이나 package-lock.json과 같은 잠금 파일이 등장하게 되었다. 패키지 잠금 파일에는 패키지가 최초로 추가될 당시 어떤 버전이 설치되었는지를 기록하고 있어, 이후 install 커맨드를 실행하여도 npm에 등록된 가장 최신 버전의 패키지가 아닌 package-lock.json에 명시된 버전으로 설치하기 때문에 같은 패키지 설치를 보장할 수 있다.
설치하기 위해선 별도의 과정을 거쳐야하는 yarn과는 다르게, npm은 Node.js를 설치하면 자동으로 딸려온다. 이런 쉬운 접근성을 바탕으로 npm은 가장 대중적인 패키지 매니저의 자리를 차지하였다. 가장 많은 사용자 보유가 가장 많은 피드백으로 이어졌는지 최근 버전의 npm은 방금 언급한 yarn의 특징을 거의 따라잡았다고 한다.
이러한 상황에서 yarn은 새로운 시도를 하기 시작했는데 2020년 등장한 Yarn Berry(Yarn의 두 번째 버전)가 그 결과물이다.
npm과 yarn은 공통적으로 node_modules를 사용하는데, node_modules 자체가 가지고 있던 비효율을 해결하고자 Yarn Berry는 node_modules를 없애는 극단적인 시도를 하였다. yarn을 이리 극단적으로 만든 node_modules의 문제는 다음과 같다.
node_modules 내부의 특정 라이브러리나 모듈을 불러오려할 때, 상위 디렉토리의 node_modules를 탐색하게 되고, 해당 탐색 과정이 반복될 수록 I/O 호출이 반복되기 때문에 큰 비효율성을 야기한다.
$ node
Welcome to Node.js v12.16.3.
Type ".help" for more information.
> require.resolve.paths('react')
[
'/Users/toss/dev/toss-frontend-libraries/repl/node_modules',
'/Users/toss/dev/toss-frontend-libraries/node_modules',
'/Users/toss/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/toss/.node_modules',
'/Users/toss/.node_libraries',
'/Users/toss/.nvm/versions/node/v12.16.3/lib/node',
'/Users/toss/.node_modules',
'/Users/toss/.node_libraries',
'/Users/toss/.nvm/versions/node/v12.16.3/lib/node'
]
예시 출처 - 토스 기술블로그
node_modules는 간단한 프로젝트에도 수백 메가바이트의 크기를 가질 정도로 큰 공간을 차지한다. 또한, 복잡한 구조를 가지고 있기 때문에, 유효한 의존성을 확인해보기 어렵다는 문제점이 생긴다.
npm과 yarn(yarn 1v을 의미함)에서는 중복해서 설치되는 node_modules 때문에 호이스팅을 적용하게 되었다.
![[Pasted image 20230629161400.png]]
좌측 이미지처럼 중복으로 설치되는 패키지를 끌어올려 우측 이미지처럼 만들어 루트 경로에서 원하는 패키지를 검색할 수 있게 되었지만, 이로 인해 오히려 직접 의존하고 있지 않은 라이브러리를 require 할 수 있는 유령 의존성 현상이 발생하게 되었다. 해당 현상은 package.json에 명시하지 않은 라이브러리를 사용할 수 있게 하거나 다른 의존성을 제거했을 때 함께 사라지는 등의 문제를 가져오게 되었다.
패키지 매니저 중 pnpm은 performant npm의 약자로, 평탄화하지 않은 node_modules를 사용하면서도 중복된 패키지 설치를 방지하는 방법을 찾아내었다. 바로 전역 저장소를 이용하는 방법인데, 이 방식은 content-addressable storage라고도 불린다. pnpm은 패키지를 설치할 때 package.json에 명시된 패키지를 읽어 node_modules에 설치하는 것이 아니라, 전역 저장소에 패키지를 설치한다. node_modules에는 symbolic link(symlinks)를 저장하여 원하는 패키지를 참조할 수 있는 구조로 만들었다.
기존의 npm이나 yarn을 사용한다면 A를 사용하는 100개의 패키지를 설치하면 A가 100번 설치가 되지만, pnpm은 이 방법을 통해 A를 필요로하는 패키지에 심링크로 연결하는 방식을 선택하여 한 번의 A 설치를 이루어내게 되었다.
Vite, Vue, Next.js 등의 다양한 라이브러리와 프레임워크가 이미 pnpm을 도입하여 만들어졌고, 다운로드 수도 급격하게 늘고 있는 상황이다.
Yarn Berry는 PnP를 통해 해당 문제들을 해결하고자 했다.
PnP는 node_modules를 사용하는 대신에 .yarn/cache에 의존성 정보를 저장하고, .pnp.cjs 에 의존성을 찾을 수 있는 정보를 저장한다.
/* react 패키지 중에서 */
["react", [
/* npm:17.0.1 버전은 */
["npm:17.0.1", {
/* 이 위치에 있고 */
"packageLocation": "./.yarn/cache/react-npm-17.0.1-98658812fc-a76d86ec97.zip/node_modules/react/",
/* 이 의존성들을 참조한다. */
"packageDependencies": [
["loose-envify", "npm:1.4.0"],
["object-assign", "npm:4.1.1"]
],
}]
]],
예시 출처 - 토스 기술블로그
이 방식을 통해 패키지 검색을 위해 비효율적으로 반복되던 I/O 작업에서 벗어날 수 있었고, 의존성 검증이 쉬워짐과 동시에 유령 의존성의 걱정에서도 벗어날 수 있다. 또한, 더 이상 node_modules 구조를 생성할 필요가 없어져 신속한 설치가 가능해졌다.
.yarn/cache 폴더는 zip 파일로 묶인 라이브러리가 저장되어 있다. yarn pnp 시스템에서 의존성을 zip 파일의 형태로 저장하게 되면서 node_modules 보다 용량을 아낄 수 있고, 의존성 관리가 쉬워진다.