제작한 라이브러리들 pnpm으로 모노레포 구축해보기

2024년 9월 4일

기존에 존재하는 메트로놈이나 웨이브폼 라이브러리들을 각각 단일 패키지로 관리해주고 있었다.

하지만 이런식으로 각각의 다른 레포지토리에서 관리했을때 독립적으로 유지보수를 해주어야하는 불편함이 있었다.

가령 하나의 라이브러리에 대한 코드를 수정한다고 가정해보자

  1. 해당 라이브러리에 대한 이슈를 등록
  2. 코드 수정 후 PR 작성
  3. 테스트 실행 후 버전 올려서 다시 배포

동일한 스크립트를 사용하고 동일한 작업을 실행하지만, 각각 다른 저장소에서 실행해주어야한다.

또한 현재 꾸준히 관리하고 있는 라이브러리는 총 2개지만,

이 2개의 라이브러리 기능을 하나로 합친 뮤직 플레이어 라이브러리 또한 추가해서 만들 계획이다.

이렇게 된다면 각각의 라이브러리 버전에 대한 의존성을 가지게 될수 밖에 없다.

예를 들어 C 라이브러리에서 A,B 라이브러리를 사용한다고 한다면

A나 B 라이브러리에 대해서 변경 사항이 발생했을때, C 라이브러리에서도 꾸준히 변경사항을 업데이트 해주어야한다.

이러한 예로 들었을때 라이브러리들이 추후에 추가 되어서 갯수가 많아질때 각각의 버전 관리가 힘들어지고 들이는 비용 또한 늘어날 수 밖에 없다고 판단했다.

그래서 이번 기회에 만든 라이브러리들에 대해서 모노레포를 구축해보았다.

- 모노레포란?

모노레포란 하나의 단일 저장소에서 여러 패키지를 함께 관리하는 방법을 의미한다.

하나의 저장소에서 여러 패키지를 관리하기 때문에, 코드 공유와 변경 사항의 추적이 훨씬 쉬워진다.

또한 공통 된 설정 파일이나 유틸 함수들을 한 곳에서 관리할 수 있기 때문에 중복 된 작업을 줄여서 효율성을 높일 수 있다.

보통 모노레포를 구축할때 pnpm이나 Yarn Berry를 많이 사용한다고 한다.

일단 본인은 왜 모노레포를 구축할때 기존에 존재하던 npm이나 yarn 이 아닌 새로운 패키지 매니저를 채택해야하는지 의문점이 들었다.

- 왜 기존 패키지 매니저가 아닌 새로운 패키지 매니저를 선택해야하는가?

모노레포의 목적은 근본적으로 효율적인 패키지 관리에 따른 개발 경험 향상에 있다.

  1. 통합 된 코드베이스

  2. 의존성 관리 최적화

  3. 효율적인 빌드 및 테스트

  4. 확장성과 유지보수성

등 단일 저장소에서 여러가지의 패키지를 효율적으로 관리함으로써 개발 경험을 향상 시키는데 목적이 있다.

pnpmyarn berry 같은 패키지 매니저는 위의 목적에 부합하는 기능들을 제공해주기 때문이다.

기본적으로 이 두개의 패키지 매니저는 기존 매니저들과 다른 방식으로 패키지를 관리한다.

pnpm

pnpm은 패키지의 의존성을 하드 링크(hard link)방식으로 공유한다.

이 하드 링크 방식이란, 동일한 파일 데이터에 여러 파일 경로가 연결 되는 것을 의미한다.

하드 링크를 통해 생성 된 파일들은 동일한 inode를 공유하며, 실제로는 하나의 파일 데이터만 저장 된다.

inode란? 아이노드는 파일 시스템에서 파일의 메타데이터를 저장하는 데이터 구조이다. 각 파일이나 디렉토리는 파일 시스템 내에서 고유한 inode를 가지고 있고, 이 inode에는 파일의 실제 데이터 위치를 포함한 여러 중요한 정보가 저장 된다.

예시를 들어보자.

프로젝트 A에서 B라는 이름을 가진 4.2버전의 패키지를 설치한다고 가정해보자.

pnpm을 통해 설치한다면, 먼저 글로벌 스토어에 B 패키지를 다운로드하여 저장하고, A 프로젝트의 node_modules 폴더에는 B 패키지 대한 하드 링크를 생성한다.

또 프로젝트 C에서 동일한 B 패키지를 설치한다고 하면, pnpm은 글로벌 스토어를 확인 하고, 이미 존재하는 B 패키지에 대해서 새로운 하드 링크를 만든다.

결과적으로 프로젝트 A와 C는 같은 inode를 가진 B 패키지를 사용하게 되므로, 디스크 공간을 절약할 수 있는 것이다.

스크린샷 2024-09-04 오후 1 37 41

그림으로 나타내면 다음과 같다.

실제적으로 B package를 동일하게 설치하는 것이 아니라, 전역 저장소에 한번만 설치해놓고 해당 패키지 정보에 대한 링크를 연결 시켜놓는다고 생각하면 된다.

A 프로젝트에서는 4.2 버전을 사용하고, C 프로젝트에서 5.3의 버전을 사용한다고 하더라도, pnpm은 각각 버전의 패키지 모두 글로벌 스토어에 저장한다.

또한 이 각각의 패키지를 서로 다른 디렉토리로 구분하므로 버전 충돌을 방지할 수 있다.


- Yarn Berry

Yarn Berry도 pnpm와 더불어 모노레포 구축에 많이 사용 되는 패키지 매니저이다.

Yarn Berry는 PnP모드와 Zero-install이라는 장점을 제공한다고 한다.

- PnP (Plug'N'Play)

PnP 모드는 Yarn Berry가 도입한 새로운 패키지 관리 방식이다.

기존에는 node_modules 폴더를 사용하여 패키지를 설치하고 관리하는데, node_modules 폴더 안에는 프로젝트가 의존하는 모든 패키지와 그 패키지들이 의존하는 하위의 다른 패키지들이 모두 다운로드 되어 저장 된다.

하지만 PnP 모드는 이와 다르게 node_modules 폴더를 생성하지 않고도 패키지를 관리한다.

이는 모든 패키지가 .yarn/cache 디렉토리에 압축 된 상태로 저장 되어있기 때문에 가능하다.

또한 yarn이 직접 패키지를 추적하고, 필요한 경우 해당 패키지를 .pnp.cjs 파일을 통해 로드하기 때문에 따로 node_modules가 필요하지 않다.

.pnp.cjs 파일은 패키지의 의존성 트리를 정의하고, 각 패키지의 위치를 정확히 지정하는 파일이다.

- Zero-install

Yarn BerryZero-install을 통해 npm install이나 yarn install없이 바로 패키지를 사용할수 있도록 한다.

이는 위에 말한 .pnp.cjs 파일과 .yarn/cache를 통해 가능하다.

yarn은 직접 패키지 경로를 추적하고 있고 cache 폴더에 이미 패키지들이 저장되어 있기 때문에 새로운 프로젝트를 체크아웃 할때마다 패키지를 다시 다운로드 할 필요가 없다.

이는 CI/CD 환경에서도 패키지 설치 과정을 생략할 수 있기 때문에 빌드 시간을 크게 줄일 수 있다.


위 두개의 패키지 매니저 중에서 본인은 pnpm을 선택했는데 이유는 다음과 같다.

기존에 만들어놓았던 라이브러리들이 전부 node_modules 폴더가 존재했는데, pnpm을 통해 기존 구조를 유지하면서도 마이그레이션을 진행하고 싶었다.

Yarn Berry를 사용하면 node_modules 없이 환경을 구축할수 있다는 것이 매력적인 부분으로 다가오긴 했다.

하지만 동시에 node_modules없이 아예 새롭게 환경이 구축 되는 것이기 때문에 의존성 문제가 터질 수밖에 없지 않나하는 생각이 들었다.

(실제로 이 글에선 Yarn Berry에서 pnpm으로 넘어갔다고 기재되어있는데, 호환성 문제도 이외에도 Ghost Dependency 문제도 존재한다.)

둘 중에 뭐가 명백히 낫다고 말할순 없으나, 지금 혼자 자료를 찾아보며 구축해보는 상황에서는 기존의 node_modules 포맷을 유지하는 것이 낫다고 판단했다.


1. pnpm 설치

기존의 node_modules 호환성을 통해 pnpm을 도입

npm i -g pnpm
pnpm init

루트 폴더에서 이제 package.json이 생성 된다.

2. pnpm-workspace.yaml 생성

packages:
  - 'packages/*'

따로 yaml 파일을 생성해서 루트 폴더에 위치시켜주어야한다.

이 파일의 역할을 다음과 같다.

  1. 워크 스페이스 생성
  • 워크 스페이스는 여러개의 패키지들의 하나의 모노레포 레포지토리에서 함께 관리되는 구조를 의미하는데, pnpm은 이러한 워크스페이스를 통해 패키지 간의 의존성을 효율적으로 관리하고, 중복된 의존성 설치를 방지한다.
  1. 패키지 범위 지정
  • pnpm-workspace.yaml 파일은 packages 속성을 통해 워크스페이스에 포함 될 패키지의 경로를 지정한다. 즉, 모노레포로 관리할 패키지들의 엔트리 포인트를 지정한다. 여기선 packages/*로 설정해줌으로써 packages 폴더 하위의 모든 패키지들을 관리해야할 패키지로 인식하게 하는 것이다.
  1. 일관된 의존성 관리
  • 워크스페이스에 속한 패키지들이 서로의 의존성을 자동으로 인식하고, 동일한 패키지가 여러 곳에서 사용될 경우, 이를 한 번만 설치하여 중복 설치를 방지한다. 이 과정에서 pnpm-workspace.yaml 파일은 각 패키지의 경로를 참조하여, 의존성 해석과 설치를 일관되게 관리하는 역할을 한다.

- package.json

{
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "pnpm -r build",
    "test": "pnpm -r test"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.7"
  }
}

루트 폴더에 생성된 package.json이다.

여기서 buildtest 스크립트를 생성해주었는데, 이러면 생성한 모든 패키지들에 대해서 테스트와 빌드를 할수 있다.

또한 버전 관리를 위한 changesets가 설치 되어 있다.

changesets에 대한 설명은 이 글에서 정리 해놓았다.

3. 패키지 설치

pnpm install
스크린샷 2024-09-04 오후 2 46 46

안에 2개의 라이브러리가 존재하는데 약 7초에 모든 패키지 설치가 끝났다.

만일 일반적인 npm install을 통해 단일 레포지토리에서 패키지를 설치하면 얼마나 걸릴까?

스크린샷 2024-09-04 오후 2 44 21

메트로놈 라이브러리 레포지토리에서 npm install을 했을때 걸린 시간이다.

하나의 패키지를 설치하는데 약 28초 정도 걸린다.

만약 2개의 패키지를 설치한다고 가정하면 50-60초가 소요 될 것이다.

또한 Github Actions과 changesets를 활용한다면, 각각 패키지의 폴더별로 버전을 관리해주고 배포 자동화를 구축해놓을 수도 있다.

- 종합적인 폴더 구조

스크린샷 2024-09-04 오후 2 36 31

Generate Tree를 사용하려했는데, node_modules까지 생성 되어서 캡쳐로 갈음 한다.

여기서 pnpm-lock.yaml이 각각 패키지에 대한 의존성을 고정/관리 해주는 역할을 한다.

패키지의 의존성 트리 구조를 상세하게 기록함으로써, 후에 pnpm이 패키지 설치 시에 중복된 의존성들을 효율적으로 관리하고 디스크 공간을 절약할 수 있도록 한다.

또한 공통적으로 eslintprettier를 설정함으로써 각각의 패키지에 공통된 룰이 적용되도록 할 수 있다.