본문 바로가기
FE/개발환경

[개발환경] 패키지 매니저별 모노레포 특징 요약(npm, yarn, yarn-berry)

by livemehere 2023. 10. 16.

여러가지 방면에서 npm 보다는 yarn, yarn-berry 가 성능면에서 우수하기 때문에 선호하고 사용해왔습니다.

npm 도 버전을 거듭하며 발전해오고 있지만, 그 사이 yarn 의 이점 때문에 yarn 에 익숙해져 굳이 npm 을 사용하지 않고, 쭉 사용하게 되는거같아요.

그러던 중에 npm 으로 모노레포 구성하는 것이 yarn 과 거의 유사해 실습해보고, yarn 과 비교해서 차이점을 분석해보고자 글을 정리합니다.

 

npm

  • workspaces 를 정의하는 순간, 모든 패키지들이 link 되어, 패키지들 간에는 어떤 의존성 설정 없이도 바로 참조가 가능해집니다.
❗️ npm 과 yarn 1.x 은 node_modules 를 사용하고, 호이스팅을 사용하면서 동일한 문제가 있으니, npm 은 간단히 사용법만 설명하고, yarn 1.x 에 자세히 설명을 해두었습니다.

workspaces 정의하기

// package.json
{
"workspaces": [
    "packages/*"
  ],
}

 

npm workspace 관련 명령어

-w 와 --workspace 는 동일한 용도이지만 문법만 살짝 다릅니다. 아래 직관적으로 작성된 스크립트를 참고해주세요

  "scripts": {
    "start:service-a": "npm run start -w service-a",
    "start:service-b": "npm run start -w service-b",
    "start:multiple": "npm run start -w service-a -w service-b",
    "start:all": "npm run start -w packages",
    "start:all2": "npm run start --workspaces"
  },

 

workspace 구성하기

  • 간단히 packages/* 하위 폴더를 workspaces 로 지정했습니다.
  • 패키지간에 어떠한 의존성도 추가하지 않았습니다.

 

service-b/index.js

function sum(a, b) {
  return a + b
}


module.exports ={
    sum
}

 

service-a/index.js

const serviceB = require("service-b");

console.log(serviceB.sum(1,2));

 

service-a/index.js 를 실행하면 잘 동작합니다.

 

 

yarn classic (1.x) 으로 구성하기

npm 과 동일한 방식으로 package.json 에서 workspace 를 수정해주고, CLI 명령어만 다릅니다.
  • 의존성을 설치해야지만, 서로 다른 패키지를 사용할 수 있습니다. 이때 버전을 명확하게 명시해줘야합니다(버전을 충족한다면, npm 레지스트리보다 로컬의 workspace 를 우선하여 설치합니다).

 

특정 workspace 에서 커맨드 실행

yarn workspace <WORKSPACE_NAME> <COMMAND_NAME>

 

workspace 간 의존 관계 확인하기

yarn workspaces info // 2.x 이후로는 yarn workspaces list

 

모든 workspace 의 명령어 실행하기

yarn workspaces run <COMMAND> // 2.x 이후로는 yarn workspaces foreach <COMMAND>

 

nohoist 옵션 사용하기

npm 과 yarn 등 모노레포를 구현한 패키지 매니저들은, 각 패키지별로 중복되는 의존성 설치를 방지하기 위해서 root 경로로 호이스팅(hoisting) 기법을 사용합니다.

 

아래 그림처럼 package-1 에서 사용하는 패키지들 중 버전까지 동일한 의존성들은 여러번 설치될 필요가 없기 때문에, root 경로로 호이스팅되어 하나만 설치하도록 합니다.

 

 

최종적으로 아래와 같은 구조가 됩니다.

 

하지만 일부 모듈 로더는 호이스팅을 지원하지 않고(metro), 또 의도적으로 패키지를 호이스팅하지 않고, 해당 패키지 내에서만 의존하도록 하고싶을 때 nohoist 속성에 해당 패키지를 명시해주면 됩니다.

 

package.json

  "workspaces": {
    "packages": [
        "packages/*"
    ],
    "nohoist": [
      "**/dayjs"
    ]
  }

glob 패턴을 사용하며, 위와 같이 작성할 경우, 모노레포의 모든 패키지에서 dayjs 패키지는 호이스팅 되지 않습니다.

 

 

yarn 2.x (berry) 소개

문제1) 호이스팅

앞서 npm 과 yarn 은 호이스팅을 사용해서 중복된 의존성 문제를 해결하였습니다.

하지만 호이스팅은 의도치 않은 side effect 를 발생시킵니다.

예를들어서 axios 라이브러리를 설치할 경우, axios 의 의존성 패키지들을 모두 호이스팅 해버려서 아래와같이 직접 패키지를 설치한것 과 같이 사용할 수 있어집니다.

const axios = require('axios');
const formData =require('form-data'); // 에러안남

 

문제2) node_modules 

node.js 에서 require() 함수로 모듈을 불러오면, 해당 모듈을 찾기위해서 상위 node_modules 를 계속해서 순회합니다. I/O 동작이 계속해서 발생하여 이미 잘 알려진 문제이기도 합니다. 또한 매우 큰 저장공간을 차지하는 단점도 있습니다.

 

🎉 위 두 문제를 해결하기 위해서 yarn 2.x 에서는 PnP 를 도입하였습니다.

 

yarn-berry 의 PnP(Plug & Play)

기존의 npm, yarn1 이 했던 것 처럼 의존 패키지들을 node_modules 로 다운받아 저장하는 대신에 패키지를 압축한 파일을 .yarn/cache 폴더에 수평적으로 저장하는데 이를 PnP 라고 부릅니다.

압축 파일들은 ZipFS를 사용하여 모듈 로드가 필요할 때 메모리에서 압축을 해제하여 접근합니다.

 

PnP 를 사용함으로써

  • 호이스팅으로 인한 유령 의존성 문제 방지
  • 수평적 저장으로 인해 모든 패키지에 대한 접근 시간 O(1) 로 단축
  • zero-install 전략으로 패키지 설치 과정 생략가능(선택)

의 이점을 누릴 수 있습니다.

 

시작하기

yarn set version berry
touch yarn.lock
yarn init -2
  • global yarn  version 이 2.x 이상 이어야합니다.
  • 패키지를 초기화하기 위해서는 yarn.lock 파일이 사전에 존재해야합니다.
  • yarn init 이 아닌 yarn init -2 를 해야합니다.

 

패키지 설치

yarn add -D vite
yarn add dayjs

 

.pnp.cjs 에 의존성 트리가 저장됩니다.

 

node 실행

yarn-berry 는 node 를 실행할 때 반드시 yarn node 로 실행해야합니다.

yarn node src/index.js // node src/index.js (x)

 

yarn 2.x (berry) 모노레포 구성하기

 

npm, yarn1, yarn2 모두 모노레포를 구성하기 위해서 package.json 에서 해줘야할 일은 동일하게 workspaces 를 정의해주는 것입니다.

"workspaces": [
    "packages/*"
],

 

모노레포 초기화

모노레포의 root 경로에서만 "-2" 를 붙여 초기화합니다.

yarn init -2

 

하위 패키지 추가 생성

mkdir packages/client
mkdir packages/ui

 

각각 초기화

❗️하위 패키지는 yarn init 만 해줍니다

yarn init

 

각 패키지별로 필요한 개발환경을 구축하고 개발합니다.

 

패키지간 의존성 만들기

정확히 버전까지 해서 불러오고자하는 패키지를 설치합니다.

yarn add ui@1.0.0
"dependencies": {
    "ui": "1.0.0"
}

 

패키지로써 잘 동작하도록 package.json 을 수정해줍니다.

// ui/package.json

{
"name": "ui",
  "version": "1.0.0",
  "packageManager": "yarn@3.6.4",
  "main": "src/index.ts",
  "exports": {
    ".": {
      "import": "src/index.ts"
    },
    "./utils": {
      "import": "./src/utils/index.ts"
    }
  }
}

 

eslint, prettier 설정

이 두 설정은 tsconfig 와 다르게 에디터의 도움을 받는 도구들입니다.

에디터는 yarn-berry 의 압축된 플러그인을 바로 해석할 수 없습니다. 이것은 알려진 이슈로써 에디터마다 해결방법이 있습니다.

저는 webstorm 을 사용하기 때문에 압축을 해제하여 에디터에 인식시켜줍니다.

yarn unplug <압축해제할 패키지명>

위 명령어로  eslint, prettier 를 해제하면 .yarn/unplugged/* 에 압축이 풀린 패키지가 생성됩니다.

 

이후 에디터를 껏다가 키면 자동으로 인식하고, 안된다면 메뉴얼하게 해당 폴더를 찾아서 지정합니다.

 

메뉴얼 하게 설정해준다면 경로는 압축해제된패키지/node_modules/패키지명 까지 설정해줍니다.

 

이 외에도 yarn-berry 는 기존의 node_modules 과 호환되는 방식들에서 충돌이 발생할텐데, 레퍼런스를 찾아서 잘 해결하시길 바랍니다. 간단히 작업한 코드 예제 를 참고해주세요.

 

마무리

지난 다른 포스트에서 yarn, yarn2 과 모노레포, 마이크로프론트엔드 구축을 다루었었는데요.

내용이 부실하기도하고, 사실 이것도 부실한거 같지만 파도파도 신경쓸껏, 충돌하는 부분이 꽤많은거 같아 한번 더 핵심을 살펴보고 정리해보았습니다.

자잘하게 할말들이 많다보니 글로남기기에는 내용이 너무 많아지네요.

기회가된다면 영상으로 한번 남기도록 해야겠습니다 :)

반응형