Yarn Berry

Southbig·2022년 11월 10일
1

Yarn Berry 란 ?

Yarn Berry는 기존의 “깨져 있는” NPM 패키지 관리 시스템을 혁신적으로 개선한다
node_modules 없이 node를 사용할 수 있도록 해준다

NPM의 문제점

NPM은 Node.js 설치 시에 기본으로 제공되어 범용적으로 사용되고 있으나, 비효율적이거나 깨져 있는 부분이 많다

비효율적인 의존성 검색

NPM은 파일 시스템을 이용하여 의존성을 관리한다
익숙한 node_modules 폴더를 이용하는 것이 특징인데, 이렇게 관리했을 때 의존성 검색은 비효율적으로 동작한다

예를 들어, /Users/toss/dev/toss-frontend-libraries 폴더에서 require() 문을 이용하여 react 패키지를 불러오는 상황을 가정하자

라이브러리를 찾기 위해 순회하는 디렉토리의 목록을 확인하려고 할 때, Node.js에서 제공하는 require.resolve.paths() 함수를 사용할 수 있다
이 함수는 NPM이 검색하는 디렉토리의 목록을 반환한다

$ 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'
]

목록에서 확인할 수 있는 것처럼, NPM은 패키지를 찾기 위해서 계속 상위 디렉토리의 node_modules 폴더를 탐색한다
따라서 패키지를 바로 찾지 못할수록 readdir, stat과 같은 느린 I/O 호출이 반복된다
경우에 따라서는 I/O 호출이 중간에 실패하기도 한다

readdir는 파일의 목록을 배열로 가져옴

I/O란?
I/O는 입력(Input)/출력(Output)의 약자로, 컴퓨터 및 주변장치에 대하여 데이터를 전송하는 프로그램, 운영 혹은 장치를 일컫는 말이다
대개의 경우 입력에 함께 출력이 발생하게 된다
단, 키보드와 마우스처럼 입력을 위한 기기, 프린터처럼 출력에만 사용되는 기기도 있다
프로세서와 메모리, 확장 슬롯, 마더보드에서 일어나는 데이터 전송도 I/O라고 일컬어진다

TypeScript 4.0까지는 node_modules를 이용한 패키지 탐색이 너무 비효율적인 나머지, 패키지를 처음으로 import 하기 전까지는 node_modules 내부의 타입 정보를 찾아보지 않기도 했다(TS 4.0 Changelog)

환경에 따라 달라지는 동작

NPM은 패키지를 찾지 못하면 상위 디렉토리의 node_modules 폴더를 계속 검색한다
이 특성 때문에 어떤 의존성을 찾을 수 있는지는 해당 패키지의 상위 디렉토리 환경에 따라 달라진다

예를 들어, 상위 디렉토리가 어떤 node_modules를 포함하고 있는지에 따라 의존성을 불러올 수 있기도 하고, 없기도 하다
다른 버전의 의존성을 잘못 불러올 수 있는 여지도 존재한다

이렇게 환경에 따라 동작이 변하는 것은 나쁜 징조다
해당 상황을 재현하기 까다로워지기 때문이다

비효율적인 설치

NPM에서 구성하는 node_modules 디렉토리 구조는 매우 큰 공간을 차지한다
일반적으로 간단한 CLI 프로젝트도 수백 메가바이트의 node_modules 폴더가 필요하다
용량만 많이 차지할 뿐 아니라, 큰 node_modules 디렉토리 구조를 만들기 위해서는 많은 I/O 작업이 필요하다

node_modules 폴더는 복잡하기 때문에 설치가 유효한지 검증하기 어렵다
예를 들어, 수백 개의 패키지가 서로를 의존하는 복잡한 의존성 트리에서 node_modules 디렉토리 구조는 깊어진다

이렇게 깊은 트리 구조에서 의존성이 잘 설치되어 있는지 검증하려면 많은 수의 I/O 호출이 필요다
일반적으로 디스크 I/O 호출은 메모리의 자료구조를 다루는 것보다 훨씬 느리다
이런 문제로 인해 Yarn v1이나 NPM은 기본적인 의존성 트리의 유효성까지만 검증하고, 각 패키지의 내용이 올바른지는 확인하지 않는다

유령 의존성 (Phantom Dependency)

NPM 및 Yarn v1에서는 중복해서 설치되는 node_modules를 아끼기 위해 끌어올리기(Hoisting) 기법을 사용한다

예를 들어, 의존성 트리가 왼쪽의 모습을 하고 있다고 가정하자

왼쪽 트리에서 [A (1.0)]과 [B (1.0)] 패키지는 두 번 설치되므로 디스크 공간을 낭비한다
NPM과 Yarn v1에서는 디스크 공간을 아끼기 위해 원래 트리의 모양을 오른쪽 트리처럼 바꾼다

오른쪽 트리로 의존성 트리가 바뀌면서 package-1 에서는 원래 require() 할 수 없었던 [B (1.0)] 라이브러리를 불러올 수 있게 되었다

이렇게 끌어올리기에 따라 직접 의존하고 있지 않은 라이브러리를 require() 할 수 있는 현상을 유령 의존성(Phantom Dependency)이라고 부른다

유령 의존성 현상이 발생할 때, package.json에 명시하지 않은 라이브러리를 조용히 사용할 수 있게 된다
다른 의존성을 package.json 에서 제거했을 때 소리없이 같이 사라지기도 한다
이런 특성은 의존성 관리 시스템을 혼란스럽게 만든다

Plug’n’Play (PnP)

Yarn Berry는 위에서 언급한 문제를 새로운 Plug’n’Play 전략을 이용하여 해결한다

Plug’n’Play의 배경

Yarn v1은 package.json 파일을 기반으로 의존성 트리를 생성하고,
디스크에 node_modules 디렉토리 구조를 만든다
이미 패키지의 의존성 구조를 완전히 알고 있는 것이다

node_modules 파일 시스템을 이용한 의존성 관리는 깨지기 쉽다
모든 패키지 매니저가 실수하기 쉬운 Node 내장 의존성 관리 시스템을 사용해야 할까?
패키지 매니저들이 node_modules 디렉토리 구조를 만드는 것에 그치지 않고, 보다 근본적으로 안전하게 의존성을 관리하면 어떨까?

Plug’n’Play는 이런 생각에서 출발했다

Plug’n’Play 켜기

NPM에서 최신 버전의 Yarn을 내려받고, 버전을 Berry로 설정하면 Yarn Berry를 사용할 수 있다

$ npm install -g yarn
$ cd ../path/to/some-package
$ yarn set version berry

Yarn Berry는 기존 Node.js 의존성 관리 시스템과 많이 다르기 때문에 하위호환을 위해 패키지 단위로만 도입할 수 있다

Plug’n’Play의 동작 방법

Plug’n’Play 설치 모드에서 yarn install 로 의존성을 설치했을 때, 기존과 다른 모습을 볼 수 있다

Yarn Berry는 node_modules를 생성하지 않는다
대신 .yarn/cache 폴더에 의존성의 정보가 저장되고, .pnp.cjs 파일에 의존성을 찾을 수 있는 정보가 기록된다
.pnp.cjs를 이용하면 디스크 I/O 없이 어떤 패키지가 어떤 라이브러리에 의존하는지, 각 라이브러리는 어디에 위치하는지를 바로 알 수 있다

예를 들어, react 패키지는 .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"]
    ],
  }]
]],

react 17.0.1 버전 패키지의 위치와 의존성의 목록을 완전하게 기술하고 있는 것을 확인할 수 있다 이로부터 특정 패키지와 의존성에 대한 정보가 필요할 때 바로 알 수 있다

Yarn은 Node.js가 제공하는 require() 문의 동작을 덮어씀으로써 효율적으로 패키지를 찾을 수 있도록 한다

이 때문에 PnP API를 이용하여 의존성 관리를 하고 있을 때에는 node 명령어 대신 yarn node 명령어를 사용해야 한다

$ yarn node

일반적으로 Node.js 앱을 실행할 때에는 package.json의 scripts 에 실행 스크립트를 등록하여 사용하게 된다
이때 Yarn v1에서 사용하던 것처럼 Yarn으로 스크립트를 실행하기만 하면 자동으로 PnP로 의존성을 불러온다

ZipFS (Zip Filesystem)

Yarn PnP 시스템에서 각 의존성은 Zip 아카이브로 관리된다
예를 들어, Recoil 0.1.2 버전은 recoil-npm-0.1.2-9a0edbd2b9-c69105dd7d.zip과 같은 압축 파일로 관리된다

이후 .pnp.cjs 파일이 지정하는 바에 따라 동적으로 Zip 아카이브의 내용이 참조된다

Zip 아카이브로 의존성을 관리하면 다음과 같은 장점이 생긴다

  1. 더 이상 node_modules 디렉토리 구조를 생성할 필요가 없기 때문에 설치가 신속히 완료된다

  2. 각 패키지는 버전마다 하나의 Zip 아카이브만을 가지기 때문에 중복해서 설치되지 않는다 각 Zip 아카이브가 압축되어 있음을 고려할 때, 스토리지 용량을 크게 아낄 수 있다

    • 실제로 토스팀에서 의존성이 차지하는 크기를 대폭 감축할 수 있었다
    • 한 서비스의 경우 NPM을 이용했을 때 node_modules 디렉토리가 약 400MB를 차지했지만, Yarn PnP를 사용했을 때 의존성 디렉토리의 크기는 120MB에 불과했습니다.
  3. 의존성을 구성하는 파일의 수가 많지 않으므로, 변경 사항을 감지하거나 전체 의존성을 삭제하는 작업이 빠르다

    • 없는 의존성이나 더 이상 필요 없는 의존성을 쉽게 찾을 수 있다
    • Zip 파일의 내용이 변경되었을 때에는 체크섬과 비교하여 쉽게 변경 여부를 감지할 수 있다

Plug’n’Play 도입 결과 장점

의존성을 검색할 때

의존성을 검색할 때, 더 이상 node_modules 폴더를 순회할 필요가 없다
.pnp.cjs 파일이 제공하는 자료구조를 이용하여 바로 의존성의 위치를 찾기 때문이다 이로써 require()에 걸리는 시간이 크게 단축된다

재현 가능성

패키지의 모든 의존성은 .pnp.cjs 파일을 이용하여 관리되기 때문에 더 이상 외부 환경에 영향받지 않는다
이로써 다양한 기기 및 CI 환경에서 require() 또는 import 문의 동작이 동일할 것임을 보장할 수 있게 되었다

의존성을 설치할 때

더 이상 설치를 위해 깊은 node_modules 디렉토리를 생성하지 않아도 된다
또 NPM이 설치하는 것처럼 같은 버전의 패키지가 여러 번 복사되어 설치 시간을 극단적으로 단축할 수 있다
이에 더해 Zero-install을 사용하면 대부분 라이브러리를 설치 없이 사용할 수 있다

이를 이용하면 CI와 같이 반복적으로 의존성 설치 작업이 이루어지는 곳에서 시간을 크게 절약할 수 있다
토스팀에서는 원래 CI에서 60초씩 걸리던 설치 작업을 Yarn PnP를 도입함으로써 수 초 이내로 단축했다

엄격한 의존성 관리

Yarn PnP는 node_modules에서와 같이 의존성을 끌어올리지 않는다
이로써 각 패키지들은 자신이 package.json에 기술하는 의존성에만 접근할 수 있다
기존에 환경에 따라 우연히 작동할 수 있었던 코드들이 보다 엄격히 관리된다
이로써 예기치 못한 버그를 쉽게 일으키던 유령 의존성 현상을 근본적으로 막을 수 있다

의존성 검증

node_modules를 사용하여 의존성을 관리했을 때에는 올바르게 의존성이 설치되지 못해서 의존성 폴더 전체를 지우고 다시 설치해야 하는 경우가 발생하고는 했다
node_modules 폴더를 검증하기 어려웠기 때문이다
전체 재설치를 수행할 때 node_modules 디렉토리 구조를 다시 만드느라 1분 이상의 시간이 허비되기도 했다

Yarn PnP에서는 Zip 파일을 이용하여 패키지를 관리하기 때문에 빠진 의존성을 찾거나 의존성 파일이 변경되었음을 찾기 쉽다
이로써 의존성이 잘못되었을 때 쉽게 바로잡을 수 있다
이로써 올바르게 의존성이 설치되는 것을 100%에 가깝게 보장할 수 있다

Zero-Install

의존성도 Git 등을 이용하여 버전 관리를 하면 어떨까?

Yarn PnP은 의존성을 압축 파일로 관리하기 때문에 의존성의 용량이 작다, 또한 각 의존성은 하나의 Zip 파일로만 표현되기 때문에 의존성을 구성하는 파일의 숫자가 NPM만큼 많지 않다
예를 들어, 일반적인 node_modules 는 1.2GB 크기이고 13만 5천개의 파일로 구성되어 있는 반면, Yarn PnP의 의존성은 139MB 크기의 2천개의 압축 파일로 구성된다

이처럼 용량과 파일의 숫자가 적기 때문에 Yarn Berry를 사용하면 의존성을 Git으로 관리할 수 있다 그리고 이렇게 의존성의 버전을 관리할 때 더욱 큰 장점들을 발견할 수 있다

이렇게 Yarn Berry에서 의존성을 버전 관리에 포함하는 것을 Zero-Install이라고 한다

의존성을 버전 관리에 포함하면 생기는 장점들

새로 저장소를 복제하거나 브랜치를 바꾸었다고 해서 yarn install을 실행하지 않아도 된다
일반적으로 다른 의존성을 사용하는 곳으로 브랜치를 변경했을 때, 잊지 않고 의존성을 설치해주어야 했다
경우에 따라서는 잘못된 의존성 버전이 사용됨으로써 웹 서비스가 알 수 없는 이유로 오동작하기도 했다

Zero-Install을 사용했을 때 이런 문제는 완전히 해결된다
더해서 네트워크가 끊어진 곳에서는 오프라인 캐시 기능을 해주기도 한다

CI에서 의존성 설치하는 시간을 크게 절약할 수 있다
Zero-Install을 사용하면 Git Clone으로 저장소를 복제했을 때 의존성들이 바로 사용 가능한 상태가 되어, 의존성을 설치할 필요가 없다
이로써 CI 시간을 크게 절약할 수 있다
Zero-install 기능을 적극적으로 레포지토리에 도입함으로써 빌드와 배포 시간을 크게 단축할 수 있다

그 외 장점

이 외에 Yarn Berry는 다양한 개발자 친화적인 기능을 제공한다

  1. 플러그인 시스템: Yarn Berry는 핵심 기능도 플러그인을 이용하여 개발되어 있을 만큼 플러그인 친화적인 환경을 자랑한다, 필요한 만큼 Yarn의 기능을 확장하여 손쉽게 CLI로 사용할 수 있다

Yarn Berry의 기능이 부족하다면 손쉽게 플러그인을 만들 수 있다

  1. 워크스페이스: Yarn Berry는 Yarn v1와 비교할 수 없을 정도로 높은 완성도의 워크스페이스 기능을 제공한다, Yarn Berry의 Git 레포지토리에서 대표적으로 사용하는 모습을 확인할 수 있다
    TypeScript를 사용함에도 한 패키지의 소스 코드의 변경사항이 즉시 다른 패키지에 반영되는 모습이 인상적이다

  2. 패치 명령어 기본 지원: 경우에 따라서 NPM에 배포된 라이브러리의 일부분만 수정해서 사용하고 싶은 니즈가 있다, Yarn Berry는 yarn patch 명령어를 제공함으로써 쉽게 라이브러리의 일부분을 수정해서 사용할 수 있도록 한다
    이렇게 만든 패치 파일은 patch: 프로토콜을 이용해서 쉽게 의존성 설치에 사용할 수 있다
    Yarn Berry를 도입함으로써 JavaScript 의존성을 효율적이고 안전하게 다룰 수 있다

profile
즐겁게 살자

0개의 댓글