Yarn Berry로 모노레포 구성하기

후훗♫·2023년 12월 5일
4

최근 회사에서 Yarn Berry로 모노레포 세팅을 했던 경험이 있다.
정리해 놓지 않으면 기억은 늘 사라지므로.. 미래의 나를 위해 정리하는 글이다.

모노레포..?

모노레포란?
여러 가지 프로젝트를 하나의 저장소에 저장하여 관리하는 소프트웨어 개발 전략

아래 그림이 모노레포에 대한 설명을 잘 나타낸 것 같다.

멀티레포는 프로젝트 별로 저장소(reppsitory)로 관리하고,
모노레포는 여러 가지 프로젝트를 하나의 저장소에서 관리한다.
출처

멀티레포, 모노레포는 개발 전략 이라고 표현한 만큼 각각의 장담점이 있다.

모노레포를 선택했던 이유는 아래와 같다.

  • 모든 코드를 단일 저장소에서 관리함에 따라 코드의 중복을 줄이고 의존성을 관리하여 개발 생산성 향상된다.
  • 하나의 저장소에서 함께 작업하기 때문에 여러가지 프로젝트 간 협업이 쉽다.
    (통합된 CI/CD 구축, MUI를 적용한 공통의 디자인 시스템 활용 등)

개발 완료 시점이 무척 중요했던 프로젝트 였던 만큼
중복 코드 최소화, 공통으로 처리가 가능한 부분 찾기 등 개발의 생상성 또한 중요했던 시기였다.

그래서.. 모노레포 세팅은 어떻게 하는거지?

모노레포로 구글링을 해보면 Yarn, Lerna, Turborepo, pnpm 등이 검색된다.

그 중 Yarn Berry를 선택했다.

22년 기준 모노레포의 tool로 가장 많이 사용하고 있고,
회사의 메인 프로젝트에서도 yarn으로 모노레포를 구성하고 참고할 수 있는 레퍼런스가 있는 것도 한 몫했다.

Turborepo 적용도 고민을 했었다.
Turborepo는 빌드가 빠르다는 장점이 있다. (변경점만 빌드를 하고, 빌드를 병렬로 처리하여 속도가 빠르다.)
그러나 기존 infra팀에서 테스트해보았을 때
yarn workspace로 쓰는 것도 크게 속도상 차이가 없었고, 동시에 빌드를 하는 경우 빌드속도가 빨라지는데,
자사에서 동시에 빌드를 하는 경우가 많진 않다고 하여 크게 장점이 되진 않는다고 결론을..내렸다고..ㅎㅎ
그래서 내가 참여한 프로젝트에서 빠르게 Yarn으로 돌아섰다는 TMI...

Yarn Berry

Yarn Berry는 Package Manager이다.

패키지 매니저(package manager)란?
프로젝트가 의존하고 있는 패키지를 효과적으로 설치, 갱신, 삭제할 수 있도록 도와주는 관리 도구입니다.
대부분의 자바스크립트 패키지 매니저는 Node.js 실행 환경(runtime)에서 돌아가며 package.json이라는 파일에 프로젝트가 의존하고 있는 패키지 목록을 명시합니다.
출처: https://www.daleseo.com/js-package-manager/

간단하게 이해하면, 패키지 매니저는 패키지 관리를 도와주는 도구이다.
하지만 조금 더 정확하게 이해하려면 모듈이란 개념에서부터 정리가 필요하다.

모듈

모듈이란 애플리케이션을 구성하는 개별적 요소로서 재사용 가능한 코드 조각을 말한다.
출처: poiemaweb - Module

모듈의 정의에서 가장 중요한 것은 재사용 가능이라고 생각한다.

프로젝트의 규모가 커질수록
같은 기능을 하는 코드를 중복해서 개발하는 것을 줄여야 개발생산성이 높다.

또 모듈간 서로의 의존성이 낮게 설계된다면,
변화의 영향을 줄일 수 있기 때문에 유지보수 관점에서도 유리하다.
(코드 하나 바꿨을 뿐인데 전체에 영향이 간다면...?ㅠㅠ)

자바스크립트의 모듈

최근 몇 년 동안 JavaScript 프로그램을 필요에 따라 가져올 수 있는, 별도의 모듈로 분할하기 위한 매커니즘을 제공하는 것에 대해 생각하기 시작했습니다.
출처: MDN - Javascript modules

자바스크립트는 별도의 모듈 기능이 없었다.
script 태그를 사용하여 별도의 파일로 구분할 수 있지만,
파일마다 독립적인 scope를 같지 않고 하나의 Global scope로 공유한다.

여러개로 쪼갠 파일이라고 하더라도 script 태그로 로드된 자바스크립트 파일은
하나의 파일안에 있는 것처럼 동작한다.
이로인해 하나의 Global scope로 인해 전역 변수가 중복되는 등의 문제가 발생한다.
모듈화가 불가능하다는 의미이기도 하다.

<script src="foo.js"></script>
<script src="bar.js"></script>
// foo.js
var a = "foo";
console.log(window.a); // foo

// bar.js
var a = "bar";
console.log(Window.a); // bar

// foo.js에서 선언한 전역변수 a는 bar.js에서 선언된 a의 값으로 재할당된다.

하지만 자바스크립트로 활용된 프로젝트가 점차 커지고 기능도 복잡해지자
모듈화에 대한 필요성이 커지게 되면서 다양한 시도를 하게 된다.

  • AMD – 가장 오래된 모듈 시스템 중 하나로 require.js라는 라이브러리를 통해 처음 개발되었습니다.
  • CommonJS – Node.js 서버를 위해 만들어진 모듈 시스템입니다.
  • UMD – AMD와 CommonJS와 같은 다양한 모듈 시스템을 함께 사용하기 위해 만들어졌습니다.
    출처: 코어자바크스립트 - 모듈소개

하지만 다행이게도(?)
ES6 이후부터 공식적으로 자바스크립트 모듈 문법인 export, import를 지원한다.
(ES Module, ESM, ECMAScriot Module.. 다양하게 불리는 것 같다.)

<script type="module" src="foo.mjs"></script>
<script type="module" src="bar.mjs"></script>
var a = "foo";
console.log(a); // foo

// bar.js
var a = "bar";
console.log(a); // bar

console.log(window.x) // undefined

// 변수 a는 더이상 window객체의 프로퍼티도 아니고, 전역 변수도 아니다!

script element에서 type attribute의 값으로 module을 추가하면 된다.
그러면 Module level Scope가 설정된다.

패키지와 패키지 매니저

조금 돌아온 느낌이지만 다시 패키지 얘기로 돌아가서..
모듈을 그룹화하면 패키지가 된다.
패키지는 원하는 기능을 구현하기 위해 필요한 모듈들을 모아둔 그룹이다.


출처

모듈과 패키지의 관계를 잘 설명해주는 그림이다.
패키지는 모듈로 구성되어있는데,
패키지는 또다른 패키지를 가지고 있기도 하다.

웹 어플리케이션을 개발하기 위해서는 여러 패키지(혹은 라이브러리)를 사용한다.
패키지가 많아질수록 패키지들 간의 의존성이 복잡하게 꼬여 해결하기 어려운 상황이 발생한다.
이를 의존성 지옥이라고도 한다.

A, B, C 모듈 3개가 있다고 할 때, A는 B v1.0을 필요로 하고 C는 B v2.0을 필요로 한다고 가정하자.

A v1.0   C v1.0
  |        |
B v1.0   B v2.0

모듈 A, C를 사용하는 프로그램을 만든다고 할때, 패키지 매니저는 B의 어떤 버전을 가져와야할까?

      Application
           |
 A v1.0  C v1.0.  B v????
 

[출처 - https://medium.com/zigbang/패키지-매니저-그것이-궁금하다-5bacc65fb05d]   

따라서 여러 가지의 패키지의 의존성을 관리해줄 도구, 패키지 매니저가 필수적이다.

Yarn Berry를 적용하면..?

npm의 문제점

의존성 탐색의 비효율성

npm하면 가장 먼저 떠오르는건 node_modules ..아닐까..?
(뭔가 되지 않을때 node_modules을 지워보는게 국룰...??..어허허..)

npm은 node_modules 디렉토리를 탐색하여 패키지를 찾는다.
즉, node_modules 디렉토리를 읽기 위해 느린 File I/O를 반복한다.

거대한 node_modules

일반적으로 많이 사용하는 프레임워크, 라이브러리를 사용해서 세팅을 하고나면
node_modules 디렉토리의 규모가 꽤 크다는 것을 확인할 수 있다.
즉, node_modules 디렉토리를 설치하기 위해 많은 시간이 소요된다.

CI/CD 관점에서도 유리하지 않다.
프로젝트가 발전할 수록 수많은 패키지가 추가되는데,
그만큼 패키지를 다운받는 시간이 증가한다는 의미이다.
(분 단위로 과금하는 CI/CD 시스템을 쓸 경우 비용이 증가한다는 소리...!!)

유령 의존성
NPM과 Yarn ver1에서는 node_modules에 중복 설치되지 않도록 Hoisting 방법을 사용한다.

이 Hoisted 된 의존성 덕분에 의존하지 않는 패키지를 사용해도 에러가 나지 않는다.
(이상한 소리 같다...????)

예를 들어보자.

출처 - rushjs.io

// my-library/package.json
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "lib/index.js",
  "dependencies": {
    "minimatch": "^3.0.4"
  },
  "devDependencies": {
    "rimraf": "^2.6.2"
  }
}
// my-library/lig/index.js
var minimatch = require('minimatch');
var expand = require('brace-expansion'); // ???
var glob = require('glob'); // ???

package.json에 보면 brace-expansionglob 패키지는 없다.
그런데 require를 해도 에러가 나지 않는다.

brace-expansionminimatch와 의존성이 있는 패키지이고,
globrimraf와 의존성이 있는 패키지이다.

package.json에 추가하지 않았지만,
Hoisted된 brace-expansionglob 패키지를 사용할 수 있게 된 것이다.

이 현상을 유령의존성(Phantom dependencies)이라고 한다.

Plug’n’Play(PnP)

위에 나열한 문제들을 yarn berry는 Plug’n’Play, PnP 방식으로 해결한다.

PnP 방식이란?
.yarn/cache에 패키지를 zip 파일로 저장하고,
.pnp.cjs 파일에 의존성을 기록해놓는다.

실제로 .pnp.cjs 에 적힌 부분을 발췌해왔다.
(프로젝트에서 이미지 슬라이더를 개발하기 위해 swiper를 적용했다.)

["swiper", [\  // 패키지 이름
        ["npm:9.4.1", {\ // 패키지 버전,
           // 패키지 경로
          "packageLocation": "./.yarn/cache/swiper-npm-9.4.1-38d46d35a3-1180b3b766.zip/node_modules/swiper/",\
          // 패키지 종속성
          "packageDependencies": [\
            ["swiper", "npm:9.4.1"],\
            ["ssr-window", "npm:4.0.2"]\
          ],\
          "linkType": "HARD"\
        }]\
      ]],\

PnP방식은 여러 가지 장점이 있다.

  • node_modules를 설치하는 대신 .pnp.cjs만 있으면 된다.
    즉, 설치가 빨라진다.

  • .pnp.cjs를 통해 디스크 I/O 없이도 의존성 정보를 확인할 수 있다.

  • Hoisting 을 하지 않기 때문에 앞서 설명한 유령의존성 현상도 나타나지 않는다.

  • 패키지를 zip 파일로 관리하므로 의존성을 구성하는 파일이 많지 않다.
    따라서 의존성의 변경이나 빠진 부분들을 찾기가 더 쉽다.

Zero Install

Yarn Berry에서 PnP와 더불어 항상 같이 언급되는 것이 Zero Install이다.

위에서 정리한대로
PnP 방식은 node_moodules을 설치하지 않고,
.yarn/cache에 zip파일로 패키지를 저장하고,
pnp.cjs 파일에 패키지의 의존성 정보를 정리한다.

그렇다면, .yarn/cachepnp.cjs 파일을 git에 등록한다면 어떤 의미일까?

보통 git clone을 통해 git이란 원격저장소에 저장된 code를 로컬PC에 저장하고,
yarn install (or npm install)로 관련 패키지를 설치한다.

그런데, .yarn/cachepnp.cjs 파일까지 git에 저장했다면,
git clone만 하면 추가 설치없이도 프로젝트에 필요한 패키지 정보를 모두 알 수 있다.

또 별도의 설치 과정이 필요 없다는 것은 CI 배포 시간 관점에서도 유리하다.

마치며..

간단하게 정리하면,
Yarn Berry로 Monorepo를 구현하기가 전부일 수 있지만,
글로 정리한 덕분에 파편화된 지식이 조금 정리된 것 같다.

그동안 소홀했던 블로그를 조금 더 활용해보아야겠다는 생각을 하면서
(혹은 다짐을 하면서, 혹은 주문을 걸면서...ㅋㅋㅋ)
글을 마친다.

출처

profile
꾸준히, 끄적끄적 해볼게요 :)

0개의 댓글