모노레포를 설명하기 전에 workspace의 개념에 대해서 알고 가자
yarn에서 잘 정의되어 있는 것 같아 가져와보면
Workspaces are the name of individual packages that are part of the same project and that Yarn will install and link together to simplify cross-references.
-> 즉, workspace란 하나의 프로젝트를 구성하는 여러 패키지들 중 하나의 단위를 의미한다고 한다 (패키지 === workspace)
그렇다면 모노레포구조를 왜 사용해야 할까?
1. 각각의 서비스들이 독립적인 레포로 관리되다 보면 유지보수가 힘들어짐
- 특정 라이브러리 업데이트, 컨벤션 변경과 같은 상황이 발생했을 때 각각의 서비스들이 독립적으로 존재하다 보면 각각의 레포에서 공통된 내용으로 PR을 올려야 함
- 각각 레포로 관리되다 보면 lint, prettier와 같은 설정이 제각각인 상황이 발생
2. 독립적인 workspace를 가져갈 수 있기 때문에 하나의 레포 안에서 작업영역을 확실하게 분리할 수 있게 됨
모노레포 세팅 방법
패키지 매니저는 pnpm을 사용할 것이며 현재 글에서는 실제 서비스 회사를 운영하는 입장에서 작성해보려 한다
모노레포의 각 workspace은 다음과 같이 3개가 되겠다
- admin - 이용고객을 관리하는 내사직원을 위한 서비스 패키지
- client - 유저에게 제공되는 실 서비스 패키지
- common - 두 서비스에서 공통적으로 사용되는 모듈을 관리하는 패키지
1. package.json구성
package.json 생성
pnpm init
현재까지의 레포구조
|- packages.json
2. pnpm-workspace.yaml 추가
packages폴더의 하위 항목들을 workspace로 정의하겠다를 선언
// pnpm-workspace.yaml
packages
- packages/*
현재까지의 레포구조
|- packages.json
|- pnpm-workspace.yaml
3. packages폴더 추가
현재까지의 레포구조
|- packages
|- packages.json
|- pnpm-workspace.yaml
그다음으로 우선 client패키지부터 구성해 보자
폴더경로를 /packages로 이동하여 아래 명령어를 실행한다(react + typescript를 통해 구성했음)
pnpm create vite
현재까지의 레포구조
|- packages
|- client
|- vite프로젝트 내용
|- packages.json
|- pnpm-workspace.yaml
이제 admin서비스를 구성해 보자
마찬가지로 /packages 경로에서 다음 명령어를 실행한다
pnpm create vite
현재까지의 레포구조
|- packages
|- client
|- vite프로젝트 내용
|- admin
|- vite프로젝트 내용
|- packages.json
|- pnpm-workspace.yaml
client, admin에서 공통적으로 사용할 모듈을 관리하는 common을 구성해 보자
리액트를 통해 컴포넌트만 다룰 것이기 때문에 의존성으로 react만 추가해 준다
// common/package.json
{
"name": "common",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \\"Error: no test specified\\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18.2.0" // <-- 추가!!
}
}
또한 common에서 export 할 공통된 모듈로써 components폴더하위에 Button.tsx을 작성해 보자
// common/components/Button.tsx
import React from "react";
interface ButtonProps {
children: React.ReactNode;
}
function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
export default Button;
현재까지 폴더구조
|- packages
|- client
|- vite프로젝트 내용
|- admin
|- vite프로젝트 내용
|- common
|- components
|- Button.tsx
|- node_modules
|- index.ts
|- packages.json
|- packages.json
|- pnpm-workspace.yaml
이제 client 패키지에서 common을 불러오기 위한 설정을 세팅해 보자
여기서 dependencies의 프로퍼티중 키값 common은 packages/common/package.json의 name키값과 동일해야 한다
// packages/client/package.json
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"common": "workspace:*", // <-- 이부분 추가!!
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
client에서 pnpm install을 해줌으로써 업데이트된 의존성을 설치해주면 된다
설치결과 client의 node_modules에 common패키지가 설치된 것을 확인할 수 있다
이제 client에서 common에 정의한 버튼을 import 하여 사용해 보자
// client
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import Button from "common/components/Button"; // <-- 이부분!!
import "./App.css";
function App() {
const [count, setCount] = useState(0);
return (
<>
<Button>common에서 불러온 버튼</Button>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;
cd packages/client
pnpm run dev
client에서 정상적으로 common의 버튼컴포넌트를 불러오는 것을 확인할 수 있다
한 가지 찝찝한 점은 client에서 Button을 import 해오는 코드 중 common/components/Button와 같이 common패키지 내부경로에 직접 접근하여 불러온다는 것을 알 수 있다
이렇게 되면 common패키지 내에서 Button컴포넌트를 관리하는 경로가 추후에 변경되어 common/Button경로에서 관리된다고 했을 때 client패키지에서 Button을 불러오는 경로를 common/Button로 재설정해줘야 하는 문제가 발생한다
따라서 common패키지내부 구조에 상관없이 사용할 수 있게 하려면 아래코드와 같이 common패키지의 index파일에서 내보낼 모듈을 재정의 해줘야 한다
// common/index.ts
export { default as Button } from "./components/Button";
이렇게 작성해 줌으로써 common패키지에서도 내보낼 파일이 무엇인지 쉽게 파악할 수 있게 되고 사용처(client, admin패키지)에서도 common의 내부 구조를 구체적으로 알 필요 없이 common이라는 일관된 경로를 통해 접근할 수 있게 된다
결과적으로 client패키지는 이제 다음과 같이 common의 index경로를 통해 가져올 수 있게 된다
// client
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import { Button } from "common"; // <-- 이부분!!
import "./App.css";
function App() {
const [count, setCount] = useState(0);
return (
<>
<Button>common에서 불러온 버튼</Button>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;
client패키지를 실행해 보면 아래와 같이 이전과 동일하게 동작한다는 것을 알 수 있다
작성하다 보니 admin패키지를 사용하지 않았는데 client패키지와 동일하게 적용하면 된다
워크스페이스란 무엇인지, 모노레포의 특징이 무엇인지 알아보았고 최종적으로 모노레포를 구성하는 방법을 처음부터 알아보았다 회사에 입사 후 모노레포를 처음 접하게 되어 직접 경험해 보면서 이해가 되지 않았던 부분을 위주로 작성해 보았다 모노레포를 처음 접하는 사람들에게 도움이 되었길 바란다