들어가기에 앞서 회사 백오피스 어드민 프로젝트를 여러 개 진행하게 되면서 모노레포의 필요성을 느끼게 되었고, 이에 회사 연구본부 방에 공유했었던 가이드를 옮겨 작성한 내용입니다.
어드민 모노레포 전환을 진행하게 되면서 얻게 된 지식들의 전파와 더불어 해당 가이드를 통해 모노레포 적용을 하심에 있어 도움이 되고자 가이드 작성 합니다.
ps. 저희도 이번에 전환하고 다양한 상황에 아직 대응해보지 못한 미흡한 부분이 있으니, 서로 트러블 슈팅 되는 부분들은 공유되었으면 좋겠습니다.
위에 그림의 모노레포 구조처럼 하나의 config 설정을 두고 각 apps 하위에 app들의 설정들을 두어 각각을 개별로 동작할 수 있게 하고, 또한 packages 하위에 공통 로직 및 compontents, utils 등을 두어 개별 앱들에서 참조 할 수 있도록 구조를 적용 하였습니다.
저희 팀은 지난 2023년부터 yarn berry를 도입하여 PnP mode와 zero-install을 통해 패키지들간의 의존성 관리를 수월하게 해왔었습니다.
이번 모노레포로 진행하게 되면서 yarn berry에서 진행하는 workspace를 통해 모노레포를 구성했어도 되었지만, pnpm을 사용한 모노레포를 구성하면서 PnP mode 뿐만 아니라 symbolic link로도 의존성 관리가 쉽게 되고 모노레포의 기본적인 기능에 충실하다고 생각하여 결정하게 되었습니다.
ps. yarn berry 든 pnpm 이든지 선택일 것 같습니다.
PNPM 설치하기
npm install -g pnpm || yarn add pnpm
pnpm-workspace.yaml
packages:
- 'apps/*' // app들을 위한 디렉토리
- 'packages/*' // 공통 package등 library를 관리하기 위한 디렉토리
루트 package.json 설정하기
{
"name": "admin-front",
"private": true,
"version": "0.0.0",
"type": "module",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"app_master_dev": "pnpm -F app-master dev",
"foodist_dev": "pnpm -F foodist-admin dev",
"ekr_dev": "pnpm -F ekr-admin dev",
"app_master_build": "pnpm -F app-master build",
"foodist_build": "pnpm -F foodist-admin build",
"foodist_deploy": "pnpm -F foodist-admin app_deploy"
},
.... 중략
}
각 앱 및 패키지 설정하기
{
"name": "app-master",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && build.bat app_master 1",
"deploy": "yarn build && ./deploy.sh",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
... 중략
}
root 디렉토리의 dependencies & devDependencies 설정
"dependencies": {
"mobx": "^6.12.0",
"mobx-react": "^9.1.0",
"mobx-react-lite": "^4.0.5",
"react-hook-form": "^7.48.2",
"react-router-dom": "^6.19.0",
"react-virtuoso": "^4.6.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.2",
"@types/node": "^20.2.5",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.42.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-svgr": "^3.2.0",
"vite-tsconfig-paths": "^4.2.0"
},
각 앱에서의 dependencies & devDependencies 설정
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@wapl/core": "0.1.0-sas",
"@wapl/superapp-websocket": "^1.0.23",
"@wapl/ui": "^0.3.1-superapp"
},
"devDependencies": {
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4"
}
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@wapl/core": "0.1.0-sas",
"@wapl/superapp-websocket": "^1.0.23",
"@wapl/ui": "^0.3.1-superapp",
"admin-common": "workspace:^0.0.1"
},
"devDependencies": {
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4"
}
"dependencies": {
"@wapl/calendar": "0.0.38-foodist",
"@wapl/core": "0.4.120-foodist",
"@wapl/foodist-coaching": "0.2.17",
"@wapl/ui": "0.2.40-foodist",
"ag-grid-community": "^30.0.0",
"ag-grid-react": "^30.0.0",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"dompurify": "^3.0.6",
"exceljs": "^4.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-csv": "^2.2.2",
"recharts": "^2.10.4"
},
"devDependencies": {
"@types/dompurify": "^3",
"@types/prop-types": "^15",
"@types/react": "17.0.2",
"@types/react-dom": "^17.0.2",
"@types/react-beautiful-dnd": "^13",
"@types/react-csv": "^1.1.3",
"@types/react-is": "^17",
"prettier": "^2.8.8",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
}
이처럼 각 환경별로 요구되는 core 및 ui 버전 혹은 각 패키지에서 주되게 사용하고 있는 버전들을 각 앱의 package.json에 명시해두어 사용하게 됩니다.
packages 하위 공통 패키지
{
"name": "admin-common",
"version": "0.0.1",
"main": "./index.ts",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ag-grid-community": "^30.0.0",
"ag-grid-react": "^30.0.0"
},
"devDependencies": {
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4"
},
"peerDependencies": {
"@wapl/ui": "*", // 같은 모노레포에서 사용하기 위해 버전
"@wapl/calendar": "0.0.38-foodist"
}
}
실제 공통 컴포넌트 사용 예시
import { Table, TableHeaderRow } from 'admin-common';
common을 사용하고자 하는 패키지에서의 package.json
"admin-common": "workspace:^0.0.1"
root 디렉토리에서 tsconfig.json 설정
{
"compilerOptions": {
/* Modules */
"baseUrl": ".",
"module": "commonjs",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"],
"@common/*": ["src/common/*"],
"@constants/*": ["src/common/constants/*"],
"@wcomponents/*": ["src/web/components/*"],
"@mcomponents/*": ["src/mobile/components/*"],
"@contexts/*": ["src/common/contexts/*"],
"@storybook/*": ["src/stories/calendar/*"]
},
"resolveJsonModule": true,
/* Type Checking */
"noFallthroughCasesInSwitch": true,
"strict": true,
"strictNullChecks": false,
"typeRoots": ["./node_modules/@types", "./src/@types"],
/* Language and Environment */
"experimentalDecorators": true,
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "ESNext", "ESNext.AsyncIterable"],
"target": "es2020",
/* Interop Constraints */
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Emit */
"declaration": true,
"downlevelIteration": true,
"inlineSources": true,
"preserveConstEnums": true,
"removeComments": false,
"sourceMap": true,
"noEmit": false,
/* Completeness*/
"allowJs": false,
"skipLibCheck": true,
"outDir": "./dist",
"noImplicitAny": false
},
"include": ["src/**/*", "**/@types/*", "**/assets/index.d.ts"],
"exclude": ["**/dist/*"]
}
하위 각 앱에서의 tsconfig 설정
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./"
}
}
2024년 3월에 모노레포를 적용하여 머지 후 한달간 진행하는 동안 별다른 큰 이슈는 발생하지 않았습니다.
추가적으로 현재 구현된 모노레포에 turborepo 도 함께 구축하여 사업건 별로 빠른 빌드 시스템 설정 및 배포 시스템을 구축 해볼 예정입니다. (TODO)
앞으로 코어나 ui 혹은 다른 프로젝트 내에서 모노레포가 적용되어 유지보수하기 쉬운 환경에 조금이나마 도움이 되었으면 좋겠습니다.
문의나 궁금한 점은 연락주시면 감사하겠습니다!