XionWCFM의 로고 이미지

Pnpm의 Workspace 기능 Catalog가 뭘까?

frontend
XionWCFM의 로고 이미지
유길종(XionWCFM)
2024.11.21. 11:34
pnpm은 9.5.0 version에서 Workspace를 위해 Catalogs라는 기능을 출시했습니다. pnpm catalogs 문서를 참고해보면 다음과 같은 소개를 볼 수 있습니다.
Catalogs" are a workspace feature for defining dependency version ranges as reusable constants. Constants defined in catalogs can later be referenced in package.json files
catalogs는 재사용 가능한 상수로 종속성의 버전 범위를 정의하기 위한 작업 공간 기능이라는 소개입니다. 핵심은 "종속성의 버전 범위를 정의하기 위한 기능" 이라고 볼 수 있겠네요
왜 워크스페이스에는 catalogs와 같은 기능이 필요했을까요? 이번에는 기존 pnpm에는 어떤 문제가 있었는지, 그리고 catalogs가 이를 해결하는데 어떤 도움을 주는지를 다루어보겠습니다.

모노레포의 문제점

모노레포를 관리하면서 가장 어려운 것은 무엇일까요? 모노레포에는 다양한 어려움이 존재하지만 그 중에서도 무시할 수 없는 어려움은 바로 버전관리의 파편화입니다.
프론트엔드 생태계에는 이제 어느 환경에서나 빠지지않고 공통적으로 사용되는 패키지들이 존재합니다. 대표적으로는 TypeScript가 있을 수 있고 테스트를 짠다고하면 테스팅 프레임워크도 공통적으로 사용할 것이고요
그런데 각 프로젝트마다 필요한 의존성들을 각 프로젝트에 개별적으로 설치할 수 밖에 없다는 한계는 오픈소스는 하루가 다르게 새로운 버전이 나오고 발전을 거듭한다는 점과 맞물려 문제로 변하게 됩니다.
한 레포지토리에서 같은 라이브러리의 여러 버전을 사용하게되는 경우 크게 두가지 문제가 발생하게됩니다.
첫번째는 개발자 경험의 하락입니다. 각 프로젝트마다 파편화되어 버전을 관리하게 되면 의존성 최신화 작업이 어려워지고 라이브러리의 동작이 어떻게 변하는지 추적하기 어렵습니다.
두번째는 디스크공간의 낭비입니다. 특히 무거운 라이브러리일수록 버전마다 설치하게되면 디스크 공간을 꽤 많이 차지하게 됩니다.
따라서 이러한 경험을 최소화하기 위해서는 모노레포를 사용할 때 단일 원천에서 공통된 configuration을 공유하고 공통된 버전을 사용하는 것이 중요합니다.

다른 패키지매니저는 이 문제를 어떻게 했을까요?


또 다른 PackageManager인 Yarn에서는 공통된 버전관리를 위해 resolutions 필드를 제공합니다.
{
  "name": "project",
  "version": "1.0.0",
  "dependencies": {
    "left-pad": "1.0.0",
    "c": "file:../c-1",
    "d2": "file:../d2-1"
  },
  "resolutions": {
    "d2/left-pad": "1.1.1",
    "c/**/left-pad": "^1.1.2"
  }
}
yarn 2 부터는 또 다른 필드인 constraints를 제공합니다.
// yarn.config.cjs
module.exports = {
  async constraints({Yarn}) {
    for (const dep of Yarn.dependencies({ ident: 'react' })) {
      dep.update(`18.0.0`);
    }
  },
};
이 예제는 프로젝트의 모든 리액트의 종속성을 18.0.0으로 고정하게 하는 Yarn2의 예제입니다.
그런데 pnpm에서는 이러한 기능을 따로 제공하지 않았습니다.
꼭 하고 싶다면 overrides 필드를 통해서 레포지토리의 버전을 고정하여 관리할 수 있었지만 그 방법이 직관적이지 않았죠
{
  "pnpm": {
    "overrides": {
      "foo": "^1.0.0",
      "quux": "npm:@myorg/quux@^1.0.0",
      "bar@^2.1.0": "3.0.0",
      "qar@1>zoo": "2"
    }
  }
}
개인적으로는 overrides 라는 단어 자체가 의존성을 고정하고 관리하는 용도라는 느낌보다는 그냥 덮어씌운다는 어감이 강하다고 생각해서 조금 거부감이 들었던 것 같아요

catalogs는 어떻게 해결하나요?

catalogs는 pnpm-workspace.yaml 에서 정의하는 것을 통해 사용할 수 있습니다.
packages:
  - packages/*

# Define a catalog of version ranges.
catalog:
  react: ^18.3.1
  redux: ^5.0.1
  typescript: ^5.6.2
이렇게 catalog 필드를 yaml 양식에 맞게 정의해두면 package.json에서 이렇게 사용할 수 있어요
    "typescript": "catalog:",
typescript의 버전을 항상 일관적으로 관리하고싶다면 catalog 필드를 이용하면 되는것이죠

또 한편으로는 이런 사용사례도 지원하고 있어요
예를 들어 특정 프로젝트에서는 react 18.3.1 버전을 사용하지만 특정 프로젝트에서는 react 16.8 버전을 사용해야하는 경우를 생각해봅시다.
catalogs:
  # Can be referenced through "catalog:react17"
  react16:
    react: ^16.8.0
    react-dom: ^16.8.0

  # Can be referenced through "catalog:react18"
  react18:
    react: ^18.2.0
    react-dom: ^18.2.0
이제 이 catalogs는 이런식으로 사용할 수 있어요
"react": "catalog:react16"

catalogs를 사용해본 경험은 어떤가요?

저는 yarn2 를 모노레포의 패키지매니저로 주로 사용해왔는데요 yarn의 pnp 방식의 문제와 아직 간혹 생태계와 잘 호환되지 않는 문제가 있어 pnpm으로 패키지매니저를 바꾸었어요
pnpm의 개발경험은 대체적으로 만족스러웠지만 모노레포에서의 버전관리에 있어 yarn의 constraints 기능과 비슷한 기능을 찾지 못해 버전관리에는 아쉬운 점이 있었는데요
catalogs는 직관적인 관리 방식을 가지고 있었고 VsCode를 사용한다면 pnpm-catalog-lens 플러그인을 통해 더욱 편리하게 사용할 수 있었습니다.
조금 아쉬운 점이 있다면 비교적 최근에 출시된 기능이다보니 아직 잘 안되는 부분들이 존재하긴 한다는 것 정도일것같아요
예를 들어 dependabot을 이용해 버전 관리를 자동화하고 있던 분들이라면 dependabot이 catalogs를 인식하지 못하는 문제를 수동으로 해결하셔야 한다는 점이 있습니다.
Add support for pnpm catalogs 이와 관련해서 dependabot-core에 PR이 열려있긴한데 병합이 되었으면 좋겠네요

마치며

catalogs는 모노레포에서의 공통된 버전관리라는 문제를 잘 풀어주는 기능이라고 생각해요 아직 사용해보시지 않았다면 한번 적용해보시는 것을 매우 추천드립니다.
읽어주셔서 감사합니다.
XionWCFM을 나타내는 캐릭터 이미지
유길종(XionWCFM)
무언가를 쉽게 설명 해낼 때 쾌감을 느끼는 사람입니다. 현재는 프론트엔드 개발자로 일하고 있습니다.