nx로 모노레포를 구축해볼 수 있는 기회가 있어서 해보았다. 정보를 얻기 위해서는 적절하지 않은 글이며, 내가 어떤 삽질을 했는지에 대한 회고에 가까운 글이다.
모노레포를 구축하게 된 계기
vue로 된 기존 레거시 프로젝트를 유지보수하게 되면서, 공통화가 전혀 안되어있는 코드 무더기를 발견하게 되었다.
예를 들면 이런 식이였다.
레거시 프로젝트는 한 페이지에 3~4개 가량의 메뉴가 존재했고, 그 메뉴들은 각각의 모듈로서 동작하며 각각 다른 repository로 분리되어있었다.
lib 폴더 내에는 모듈에서 사용하는 util성 코드들이 있었는데, 가령 date와 관련된 코드들이 있는 date.js
는 각 모듈에서 공통적으로 사용하는 코드이다보니, 각 모듈에서 복사 붙여넣기를 통해 사용하고 있었다.
내부적인 커뮤니케이션을 통해 공통화할 코드를 뽑아내고, 그 코드를 공통 모듈로 분리하여 사용하자는 의견이 나왔다.
그렇게 해서, 테스트로 아주 작은 모노레포
만들기를 시작하게 되었다.
조사한 것들
- 다른 라이브러리들은 어떤방식으로 구성했는가?
- 모노레포 빌드시스템
- 번들러
- 테스팅툴
- 문서화 어떻게 할 것인지
모노레포를 구성하기 전에 필요한 사전조사들을 위와 같이 진행했고, 같이 테스트를 진행하기로 한 팀원분은 turborepo, 나는 nx를 선택해 테스트를 진행하기로 했다.
nx를 선택한 이유
- turborepo에 비해 풍부한 생태계
- 분산 캐싱 (turborepo, nx 둘다 지원)
- 초기 모노레포 개발 환경 구축 비용을 크게 줄여줌.
- 의존성 그래프 시각화 기능 (turborepo, nx 둘다 지원)
nx로 모노레포 구축해보기
- 구축환경 :
nx
,vite
,pnpm
테스트해보려고 하는 것.
- nx, vite, pnpm으로 모노레포를 구성한다.
- lib 내의 common성 util 코드를 export하는 common 패키지와 vue 컴포넌트를 export하는 vue 패키지로 구성한다.
- 만든 공통모듈을 npm에 publish 한다.
- publish한 공통모듈을 vue 프로젝트 내에서 import해서 사용해본다.
- changeset을 통해 버전관리를 해본다.
- vitest로 테스트코드를 작성해본다.
pnpm 설치
npm install -g pnpm
yarn global add pnpm
nx로 프로젝트 생성하기
npx create-nx-workspace --pm pnpm
🚨 계속 nx cloud 설정이 안되어있다는 에러.
-> ✅ Enable distributed caching to make your CI faster · No 를 선택하니까 nx cloud를 자동으로 설치하지 않았음.
패키지 생성하기
nx g npm-package packages/common
nx g npm-package packages/vue
🚨 nx command를 사용할 수 없는 에러
-> ✅ nx global 설치
yarn global add nx
package.json에 workspaces 설정
{
"name": "@core-monorepo/source",
"version": "0.0.0",
"license": "MIT",
"workspaces": ["packages/*"],
...
}
그러면 폴더구조는 이렇게 됨.
├── packages
│ ├── common
│ │ ├── CHANGELOG.md
│ │ ├── dist
│ │ ├── node_modules
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── project.json
│ │ ├── src
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ └── vue
│ ├── CHANGELOG.md
│ ├── dist
│ ├── node_modules
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── project.json
│ ├── src
│ ├── tsconfig.json
│ └── vite.config.ts
common 패키지 작성
dependency 추가
pnpm add dayjs
vite.config.js 작성
import { defineConfig } from "vite"
import path from "path"
import dts from "vite-plugin-dts"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [
// TypeScript 소스 코드에서 자동으로 타입 정의 파일 생성.
dts(),
// tsconfig.json 파일에서 정의한 경로 별칭을 사용해 모듈 경로를 해석
tsconfigPaths(),
],
build: {
lib: {
// 진입점 파일 경로를 지정
entry: path.resolve(__dirname, "src/index.ts"),
name: "@core-monorepo/common",
// commonjs와 ES 모듈을 빌드
formats: ["es", "cjs"],
fileName: format => `core-common.${format}.js`,
},
},
})
package.json에 script 작성
{
"scripts": {
"build": "tsc && vite build"
}
}
vite로 build
pnpm run build
vue 패키지 작성
package.json에 common 패키지 dependency 추가
- vue 컴포넌트에서 common 패키지 내의 util 코드를 사용하기 위해 common 패키지를 dependency로 추가해준다.
{
"dependencies": {
"@nx-monorepo/common": "../common/dist"
}
}
-> npm publish 이후에 수정해줄 것.
vite.config.js 작성
import { defineConfig } from "vite"
import path from "path"
import dts from "vite-plugin-dts"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [
dts({
insertTypesEntry: true, // 컴포넌트 타입 생성
}),
tsconfigPaths(),
],
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
},
},
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "@core-monorepo/vue",
formats: ["es", "cjs"],
fileName: format => `core-vue.${format}.js`,
},
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
},
},
})
🚨 vue 패키지를 빌드하기 위한 간단한 vue 컴포넌트를 작성했는데, index.ts에서 export가 안됐다.
-> ✅ 문제는 env
파일의 부재였다.
env 파일 생성
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
shims-vue.d.ts
그러면 이 파일이 뭔지에 대해 알아보자.
TypeScript 프로젝트에서 Vue.js 단일 파일 컴포넌트(
.vue
파일)를 사용할 때 필요한 TypeScript 타입 정의 파일
이 파일을 사용하면 Vue.js 컴포넌트를 타입 안전하게 가져와서 사용할 수 있다.
그래서 왜쓰는데..?
보통 .vue
파일에는 HTML 템플릿, JavaScript 코드, 그리고 CSS 스타일이 포함되어 있는데 이렇게 세 유형이 함께 있는 파일을 TypeScript에서 정확하게 타입 검사하기는 어렵기 때문에 shims-vue.d.ts
파일이 ts 환경에서 사용하기 쉽도록 도와준다.
🚨 vue package를 vite로 build할 때 parse error가 났다.
-> ✅ vite.config.js에 vite-plugin-vue2의 createVuePlugin
를 import 해줌.
import { createVuePlugin as vue } from 'vite-plugin-vue2';
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
tsconfigPaths(),
],
...
});
createVuePlugin 추가해줘야하는 이유
vite-plugin-vue2
: Vue 2를 지원하는 Vite 플러그인
vite-plugin-vue2
를 사용하려면 해당 플러그인에서 제공하는 createVuePlugin
함수를 vite.config.js
파일에서 가져와야 한다.
이 함수는 Vue 2 를 위한 Vite 플러그인을 생성하는 데 사용된다.
🚨 Rollup failed to resolve import "@nx-monorepo/common"
-> ✅ vite.config.js 에 resolve.alias를 추가해준다.
export default defineConfig({
...
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
'@nx-monorepo/common': path.resolve(__dirname, '../common/dist'),
},
},
});
illegal operation on a directory
-> ✅ dist에 대한 경로가 잘못되어있었다.
export default defineConfig({
...
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
// dist 경로 수정
'@nx-monorepo/common': path.resolve(__dirname, '../common'),
},
},
});
npm publish
npm login
npm login
🚨 403 Forbidden Permission denied
-> ✅ github token받아야함
- 오른쪽 상단에 있는 사용자 이미지를 클릭하고 "Settings"를 선택.
- 왼쪽 메뉴에서 "Developer settings" 아래에 있는 "Personal access tokens"를 선택
- "Generate token"을 클릭하여 새 토큰을 생성. "write:packages"와 "read:packages" 선택.
- npm config set
npm config set //npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
🚨 402 Payment Required - You must sign up for private packages
- package.json 에서 "private": false 를 추가해도 여전히 같은 에러가 떴다.
- npm logout 후에 다시 해도 여전했음.
-> ✅ publish 시에 --access public 옵션을 추가해준다.
npm publish --access=public
🚨 404 Not Found - Scope not found
-> ✅ 오가니제이션 추가
-> 드디어 성공! 🎉
vue project에서 core-monorepo/vue 패키지를 import해서 사용하기
- publish된 vue 패키지를 vue 프로젝트에서 import해서 사용해보자.
- vue package에서 common package의 로컬 경로를 바라보고 있어서, 이 부분을 실제 publish된 패키지를 바라보도록 수정해준 뒤 다시 publish 해주었다.
🚨 Module not found: Error: Can't resolve '@core-monorepo/vue'
- build된 결과물도 뒤지고, index.ts를 수정해보기도 하고 갖은 삽질을 다했는데 계속 같은 에러가 떴는데 엄청 허무하게 해결됐다.
-> ✅ rollupOptions의 hierarchy가 잘못되어있었다.
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "@core-monorepo/vue",
formats: ["es", "cjs"],
fileName: format => `core-vue.${format}.js`,
},
},
rollupOptions: {
external: ["vue", "@core-monorepo/common"],
output: {
globals: {
vue: "Vue",
},
},
},
})
export default defineConfig({
build: {
lib: {
...
},
rollupOptions: {
external: ['vue', '@core-monorepo/common'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
});
- vite.config 파일을 js로 작성했더니 발견하지 못했던 아주 사소한 에러였다. 🫨
changeset 으로 버전관리하기
1. pnpm-workspace.yaml 파일 생성
packages:
- packages/* # 워크스페이스 패키지의 경로
2. @changesets/cli 설치
- 워크스페이스 패키지로 changesets 설치
pnpm add -w --save-dev @changesets/cli
-w
옵션
--workspace-root
플래그사용 여부에 대한 옵션- 일반적으로 nx 모노레포에서는 워크스페이스 루트에서 패키지를 설치하는 것이 좋다.
워크스페이스 내의 모든 프로젝트가 공통 패키지를 공유할 수 있으며, 패키지 간의 종속성 충돌을 방지할 수 있다.
3. changeset init
npx changeset init
4. 변경사항 생성 후, npx changeset
npx changeset
- 변경할 패키지, 버전 등 선택 (선택할때 스페이스바로 선택해야한다.)
5. changeset version
npx changeset version
vitest로 테스트코드 작성하기
1. vitest 설치
pnpm add -w --save-dev vitest
2. vitest.config.ts 작성
import vue2 from "@vitejs/plugin-vue2"
import { fileURLToPath } from "node:url"
import path from "path"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [vue2()],
// test 코드 내에서 @ alias 사용을 위해 셋팅
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
// 브라우저 환경에서 테스트하는 것을 명시
environment: "jsdom",
root: fileURLToPath(new URL("./", import.meta.url)),
},
})
3. jsdom 설치
pnpm add -w -D jsdom
✅ vue 버전이 2.7.0 미만이라면, vue, vue-template-compiler 버전 업데이트 해준다.
docusaurus로 문서화
package 경로 밑에서 docusaurus 프로젝트 생성
npx create-docusaurus
→ ✅ node 버전과 npm 버전이 맞지 않는다는 오류가 뜨면 버전 업데이트를 해주면 된다.
끝!
- 두서 없고 미흡한 글이지만, 많은 삽질 끝에 결국 모노레포 구축에 성공한 걸 기념(?)으로나마 글로 남기고 싶었다.
참고자료
https://if1live.github.io/posts/escape-from-jest-jest-is-slow/
https://velog.io/@saeeng/VITE-VITEST-migrate-from-Jest
https://velog.io/@gu2144/Turborepo로-모노레포-개발-경험-넓혀보기
https://github.com/sungkwangkim/yarn-berry-test/tree/8-final
https://engineering.linecorp.com/ko/blog/monorepo-with-turborepo
https://d2.naver.com/helloworld/7553804