안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] yarn berry (vs npm, yarn v1)

피그브라더 2022. 11. 13. 16:01

Node.js를 설치할 때 기본적으로 npm이라는 패키지 관리자가 함께 설치되며, 아직까지는 이게 가장 많이 사용되고 있다. 그러나 npm은 의존성 관리 방식에 있어서 비효율적이거나 문제가 되는 부분이 꽤 존재한다. 이를 해결하기 위해 yarn 개발자가 버전 2의 yarn을 새로 출시하였는데(2020.01.25), 이때부터는 yarnpkg/berry 저장소에서 별도로 유지 보수하기 시작했고 이를 yarn berry라 부르게 되었다. 그렇다면 npm이나 yarn v1은 어떤 문제를 가지고 있었는지, 그리고 새로 등장한 yarn berry는 그러한 문제를 어떻게 해결한 것인지 한 번 알아보자.

 

1. yarn berry (feat. Plug'n'Play)

우선, yarn berry를 사용하는 방법은 다음과 같다. npm을 이용하여 최신 버전의 yarn을 전역으로 설치하고, yarn berry로 패키지를 관리하고 싶은 프로젝트의 폴더로 이동하여 berry 버전을 설정하면 된다.

npm install -g yarn
cd my-project
yarn set version berry

그러면 프로젝트 폴더에 package.json, .yarnrc.yml 파일과 .yarn 폴더가 생성되어 있을 것이다. 그렇다면 이제 예시로 react 패키지를 설치해보고 yarn berry가 패키지를 어떤 방식으로 관리하는지 알아보자.

yarn add react

설치가 완료된 후 프로젝트 폴더의 구조를 살펴보면 다음과 같을 것이다.

npm이나 yarn v1과 달리 yarn berry는 node_modules 폴더를 생성하지 않는다는 사실을 관찰할 수 있다. 대신, 필요한 버전의 패키지를 .yarn/cache 폴더에 압축 파일(.zip)의 형태로 저장하고, 각 버전의 패키지가 어떤 위치에 있고 어떤 패키지를 직접 의존하는지의 정보를 담은 .pnp.cjs 파일을 생성한다. .pnp.cjs 파일의 내용을 한 번 간단히 살펴보면 다음과 같다.

#!/usr/bin/env node
/* eslint-disable */
"use strict";

function $$SETUP_STATE(hydrateRuntimeState, basePath) {
  return hydrateRuntimeState(JSON.parse('{\
    ...
    "packageRegistryData": [\
      [null, [\
        [null, {\
          "packageLocation": "./",\
          "packageDependencies": [\
            ["react", "npm:18.2.0"]\
          ],\
          "linkType": "SOFT"\
        }]\
      ]],\
      ["js-tokens", [\
        ["npm:4.0.0", {\
          "packageLocation": "./.yarn/cache/js-tokens-npm-4.0.0-0ac852e9e2-8a95213a5a.zip/node_modules/js-tokens/",\
          "packageDependencies": [\
            ["js-tokens", "npm:4.0.0"]\
          ],\
          "linkType": "HARD"\
        }]\
      ]],\
      ["loose-envify", [\
        ["npm:1.4.0", {\
          "packageLocation": "./.yarn/cache/loose-envify-npm-1.4.0-6307b72ccf-6517e24e0c.zip/node_modules/loose-envify/",\
          "packageDependencies": [\
            ["loose-envify", "npm:1.4.0"],\
            ["js-tokens", "npm:4.0.0"]\
          ],\
          "linkType": "HARD"\
        }]\
      ]],\
      ["react", [\
        ["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"\
        }]\
      ]],\
      ["root-workspace-0b6124", [\
        ["workspace:.", {\
          "packageLocation": "./",\
          "packageDependencies": [\
            ["root-workspace-0b6124", "workspace:."],\
            ["react", "npm:18.2.0"]\
          ],\
          "linkType": "SOFT"\
        }]\
      ]]\
    ]\
  }'), {basePath: basePath || __dirname});
}

...

만약 require() 또는 import로 특정 패키지를 불러온다면, 위와 같은 .pnp.cjs 파일의 내용에 근거하여 해당 패키지의 탐색이 동적으로 이뤄진다. 이 방식으로 패키지를 관리하는 yarn berry의 전략을 Plug'n'Play, 또는 줄여서 PnP라고 부른다.

 

그런데 눈치챈 사람도 있겠지만 이 방식이 적용되려면 기본적으로 Node.js에서의 require() 함수와 import 구문이 오버라이딩되어야 한다. 이를 위해서는 node 명령어 대신 yarn node 명령어를 사용하면 된다. 그리고 package.json 파일에 등록된 스크립트를 yarn 명령어로 실행하게 되면(EX. yarn dev) 자동으로 PnP 방식을 사용하게 된다.

 

2. npm, yarn v1과의 비교

2-1. 패키지 탐색

npm과 yarn v1의 경우, 특정 패키지를 탐색하기 위해서는 해당 패키지를 발견할 때까지 상위 디렉토리의 node_modules 폴더를 확인하는 과정을 반복한다. (require.resolve.paths() 함수를 사용하면 특정 패키지의 탐색을 위해 디렉토리들을 순회하는 경로를 알아볼 수 있다.) 따라서 패키지를 찾지 못할 때마다 계속해서 상위 디렉토리로 올라가게 되고, 이로 인해 비효율적인 I/O 연산이 반복되어 탐색에 굉장히 많은 시간이 소요된다. 기본적으로 디스크 대상의 I/O 연산은 무겁기 때문이다.

 

반면에 yarn berry의 경우, 디렉토리들을 순회할 필요 없이 .pnp.cjs 파일의 정보를 이용하여 필요한 패키지의 위치를 바로 찾을 수 있기 때문에 비효율적인 I/O 연산을 최소화한다. 따라서 패키지 탐색(= 모듈 임포트) 속도가 월등히 개선된다.

 

2-2. 상위 디렉토리 환경에 대한 의존성

npm과 yarn v1의 경우, 앞서 설명했듯이 특정 패키지를 탐색하려면 상위 디렉토리들을 확인해야 하기 때문에, 어떤 패키지의 탐색 성공 여부는 곧 상위 디렉토리 환경이 어떤지에 따라 결정된다. 이는 동일한 프로젝트여도 환경에 따라 특정 패키지의 사용 가능 여부가 달라질 수 있고, 문제가 되는 어떤 상황을 재현하기 어려워진다는 문제점을 내포한다.

 

반면에 yarn berry의 경우, .pnp.cjs 파일을 이용하여 패키지를 탐색하므로 더 이상 상위 디렉토리 환경에 영향을 받지 않는다. 따라서 동일한 프로젝트라면 어떤 환경에서 설치되든지 패키지 탐색(= 모듈 임포트)과 관련해서는 동일한 동작을 보장할 수 있게 된다.

 

2-3. 유령 의존성 (Phantom Dependency)

npm과 yarn v1의 경우, 동일한 버전의 패키지가 중복해서 설치되지 않도록, 그리고 의존성 트리의 깊이가 최소화되도록 하는 호이스팅(Hoisting) 전략을 사용한다. (이와 관련한 자세한 내용은 이 포스팅을 참조) 그런데 이러한 호이스팅은 프로젝트가 직접 의존하고 있지 않은 어떤 패키지를 직접 의존한 것처럼 불러와 사용할 수 있는 유령 의존성 문제를 낳고, 이로 인해 특정 패키지가 프로젝트에서 제외되면 그렇게 몰래 사용할 수 있던 패키지가 갑자기 사용할 수 없게 되는 문제를 낳기도 한다. 이러한 문제는 의존성 관리 체계에 혼란을 가져다준다.

 

반면에 yarn berry의 경우, 호이스팅을 사용하지 않고 직접 의존한 패키지만 불러와 사용할 수 있다. 즉, 의존성이 엄격하게 관리되기 때문에 의존성 관리 체계에 불필요한 혼란이 야기되지 않는다.

 

2-4. 패키지 설치 및 의존성 검증

npm과 yarn v1의 경우, node_modules 폴더가 매우 큰 용량을 차지한다. 또한, 단순히 용량만 큰 것이 아니라 내부 구조가 복잡하고 깊기 때문에 설치를 위해서는 비효율적인 I/O 연산이 굉장히 많이 필요하여 설치 속도도 느리다. 그리고 node_modules 폴더의 내부 구조가 복잡하고 깊다는 것은 의존성의 검증이 어렵다는 것을 의미하기도 한다. 하나하나 모두 살펴봐야 하는데 이 또한 비효율적인 I/O 연산을 굉장히 많이 필요로 하기 때문이다. 그래서 npm이나 yarn v1에서는 기본적으로 의존성 트리가 올바른지만 검증하고 각 패키지가 제대로 설치되어 있는지까지는 검증하지 않는다. 그래서 패키지 설치에 문제가 있는 듯싶으면 그냥 node_modules 폴더를 아예 지우고 다시 설치하는 게 가장 바람직한데, 앞서 말했듯 이러한 재설치 과정에 꽤나 많은 시간이 소요되는 것이 문제이다.

 

 

반면에 yarn berry의 경우, 각 버전의 패키지를 하나의 압축 파일(.zip)로 관리하기 때문에, 복잡하고 깊은 구조의 node_modules 폴더가 필요 없어짐과 동시에 설치되는 파일의 개수와 용량 모두 확연하게 줄어든다. 따라서 비효율적인 I/O 연산의 횟수가 줄어 설치 속도를 매우 단축할 뿐 아니라, 패키지 자체를 Git 등의 버전 관리에 포함시키는 Zero Install 전략을 활용하여 아예 설치가 필요 없도록 만들 수도 있다. Zero Install을 활용할 경우 새로 저장소를 Clone 하거나 브랜치를 바꾼다고 해서 패키지들을 다시 설치해줄 필요가 없으며, CI/CD에 소요되는 시간도 굉장히 많이 단축된다. 그리고 압축 파일(.zip)로 패키지를 관리하므로 의존성 트리뿐 아니라 각 패키지 자체에 대한 검증까지도 정확히 이뤄진다. 각 압축 파일의 변경 여부를 비교하면 되기 때문이다.

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://toss.tech/article/node-modules-and-yarn-berry