모노레포란?
다수의 프로젝트를 한 개의 레포지토리 내에서 관리하는 소프트웨어 개발 전략 - wikipedia
회사에 입사 후 프로젝트는 모노레포 구조로 관리되고 있었습니다. Clean Architecture라는 구조를 지향하며 공통된 환경(eslint, prettier)을 공통적으로 관리함과 동시에 재사용성이 높은 UI와 비지니스 로직을 공유하며 개발 경험을 향상 시킬 수 있는 것을 알게 되었습니다. 그러던 중 매번 사이드 프로젝트를 시작할 때나 테스트를 위해 매번 CRA을 통해 프로젝트를 생성해 왔고 각 프로젝트에 맞는 tsconfig.json, next.config.js 등의 typescript, react, next환경에 맞게 설정하는 시간과 매번 같은 목적으로 사용되는 Button, Input등의 기본 컴포넌트들을 새로 만드는 시간을 Design System을 통해 범용적인 컴포넌트를 만들어 재사용할 수 있지 않을까 라는 생각을 하게 되었습니다.
1️⃣ tsconfig.json, eslint, prettier등 코드 문법, 규칙, ts컴파일 등의 공통 설정 사항 공유하기
2️⃣ 범용성 높은 컴포넌트를 만들어 각 프로젝트에서 공유하기
제가 사용하던 패키지 매니저로는 npm, yarn, pnpm
이 있었습니다. 사실 지금까지 사용해보며 그저 react, next등의 패키지들을 설치/삭제의 용도로 사용하고 프로젝트에서 사용하고 있던 패키지 매니저를 선택하며 큰 의미를 두지 않았습니다. 하지만, 이번 모노레포로 구축하며 모노레포로 구축하기 위해서 어떤 패키지 매니저를 사용할지 고민하게 되었고 pnpm
으로 결정하게 되었습니다.
npm은 매 프로젝트마다 패키지 카피를 설치하여 disk공간을 낭비합니다. 반면 pnpm은 모든 패키지들을 .pnpm-store에 저장하며 각 프로젝트들은 이 패키지들을 공유합니다.
pnpm은 npm, yarn과 달리 dependency들을 단일 루트 하에 평평하게 위치 시킵니다(hoisting). 패키지를 설치할 때, package.json에 명시된 패키지를 읽은 후 node_moudles에 Symbolic Link(symlink)을 생성하여 전역 저장소의 해당 패키지를 참조합니다. 이 방식을 통해 명시한 패키지만 사용할 수 있게 됩니다.
예를 들어 각 프로젝트에서 react, next로 구축되어 있다고 가정한다면 둘다 react 패키지가 필요합니다. pnpm은 최상단 폴더의 .pnpm/node_moudles
로 패키지를 설치하고 두 프로젝트 모두 이 패키지를 가리키도록 설정하여 disk 낭비를 줄입니다. 추후 실제 설치과정에서 다시 설명하도록 하겠습니다.
pnpm-workspace는 간단하게 모노레포 서정을 할 수 있습니다. pnpm-workspcae.yaml 파일에 모노레포를 적용할 폴더를 명시하고 package.json
에 간단한 설정을 해 주면 끝입니다. 만들어진 패키지들은 자동으로 링크되기에 별도로 설치나 연결해줄 필요가 없습니다.
꼭 pnpm이 아니어도 됩니다!
하지만 최근 npm v9.4 부터는 isloated node_modules 기능을 지원하여 기존 npm 문제들을 해결할 수 있는 것으로 보이지만, 회사에서도 pnpm을 사용하고 모노레포의 구조를 pnpm으로 구성하였기네 이에 대한 역량을 키우기 위해 pnpm을 선택하게 되었습니다.
먼저 pnpm으로 구성할 모노레포 프로젝트의 폴더를 생성하여 폴더 내에서 아래의 명령어를 칩니다.
pnpm init
그리고 pnpm-worksapce.yaml
파일을 만들어 pnpm이 packages, apps내의 패키지를 workspace 패키지로 인식하게 합니다.
# pnpm-workspace.yaml
packages:
- "packages/*"
- "website"
먼저 코드 규칙, ts 설정 등을 위해 아래의 패키지들을 설치해 줍니다.
pnpm add -D typescript @types/node -w
pnpm add -D prettier -w
pnpm add -D eslint@9 eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin -w
-w
옵션은 최상위 폴더에 설치하겠다는 뜻입니다. 필요한 패키지들을 설치하고 tsconfig.json
, .prettierrc
, .eslint.config.js
파일들을 만들어 기본 규칙을 추가합니다. prettier, eslint의 경우 최상위 폴더에 위치시켜 규칙을 공유하며 tsconfig.json은 최상위 폴더에 tsconfig.base.json 을 만들어 기본 ts 컴파일 규칙을 추가하고 extends
을 통해 공유하며 프로젝트에서 추가적인 규칙은 따로 정의할 예정입니다.
website 폴더내에 vite을 사용하여 프로젝트를 하나 생성합니다.
cd website
pnpm create vite
그리고 공용 로직을 사용할 폴더를 utils
로 만들고 tsconfig.json 파일을 만들어 기존에 최상위 프로젝트에서 작성한 tsconfig.base.json
을 extends 에 작성합니다
# utils/tsconfig.json
{
"extends": "../../tsconfig.base.json"
}
유틸함수를 테스트를 위한 vitest와 typescript 패키지를 설치하고 아래와 같이 package.json
을 구성합니다.
{
"name": "@mono/utils",
"version": "0.0.1",
"main": "src/index.ts",
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"typescript": "~5.7.2",
"vitest": "^3.0.8"
}
}
main 필드에 src/index.ts
을 추가해 주세요
그리고 간단한 더하기 유틸 함수를 만들고 테스트를 위한 테스트 코드도 작성합니다.
// calc.ts
export const add = (a: number, b: number) => a + b;
import { test, expect } from "vitest";
import { add } from "./calc";
test("add(10, 20) should return 30", () => {
expect(add(10, 20)).toBe(30);
});
index.ts 파일에 꼭 생성한 add 유틸 함수를 export 해주세요!
이제 공용 유틸 함수도 구성했으니 재사용성 컴포넌트를 작성할 프로젝트를 생성합니다. 역시 vite
을 통해 생성했으며 불필요한 파일을 삭제하였습니다.
pnpm create vite
delete folder
- public
- src
이전과 같이 tsconfig.json 파일에 extends
키워드로 기존 tsconfig.base.json
파일을 받았으며 테스트를 위한 tsconfig.app.json
파일에 types 필드를 추가하였습니다
// tsconfig.app.json
...
...
"types": ["@testing-library/jest-dom"]
마지막으로 간단한 테스트용 Button
컴포넌트를 만들었습니다. index.ts, Button.tsx, Button.test.tsx 파일을 만들었습니다.
// Button.tsx
import { ComponentProps } from "react";
const Button = (props: ComponentProps<"button">) => {
return <button {...props} />;
};
export default Button;
// Button.test.tsx
import { test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import Button from "./Button";
test("Button shuold be rendered", () => {
render(<Button>Hello</Button>);
expect(screen.getByText(/Hello/)).toBeInTheDocument();
});
// index.ts
export { default as Button } from "./Button";
또한 package.json 파일의 main 필드에 src/index.ts을 추가해주세요!
마지막으로 테스트를 위해 vite.config.ts
파일을 아래와 같이 작성하였습니다. 지금까지 작성한 코드는 아래 사이트를 참고했으니 더 자세한 사항은 아래 사이트를 참고해 주세요.
코드 참고: React Monorepo Setup Tutorial with pnpm and Vite: React project + UI, Utils
// vite.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
export default defineConfig(({ mode }) => ({
plugins: [react()],
resolve: {
conditions: mode === "test" ? ["browser"] : [],
},
test: {
environment: "jsdom",
setupFiles: ["./vitest-setup.js"],
globals: true,
},
}));
테스트와 공용 컴포넌트, 공용 유틸 함수를 여러 프로젝트에서 공유해서 사용할 준비가 끝났습니다. 이제 테스트를 위해 최상위 디렉토리의 package.json
에 아래 스크립트를 추가해 테스트를 진행하면 되겠습니다.
// package.json scripts field
"test:all": "pnpm -r test",
"dev": "pnpm --filter website dev",
pnpm test:all
테스트가 성공적이었습니다. 이제 버튼을 import 받아 실제 프로젝트에서 테스트 해보겠습니다.
먼저 website
의 package.json 파일에서 이전에 작성한 pakcage.json
의 name필드를 바탕으로 패키지로 설치함을 명시합니다.
// website/package.json
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@mono/ui": "workspace:*",
"@mono/utils": "workspace:*"
그리고 vite로 생성한 프로젝트에 Button과 add 유틸함수를 추가해 작동하는지 확인해 보겠습니다.
import { useState } from 'react';
import reactLogo from './assets/react.svg';
import viteLogo from '/vite.svg';
import './App.css';
import { Button } from '@mono/ui';
import { add } from '@mono/utils';
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vite.dev">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
// add 함수 사용
<button onClick={() => setCount((count) => add(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>
// 버튼 컴포넌트 사용
<Button>Button from ui</Button>
</>
);
}
export default App;
기존 setCount에 add 함수를 사용했으며 아래 Button
컴포넌트를 추가하고 프로젝트를 실행하였습니다.
버튼 클릭시마다 count가 추가되었으며 Button from ui
버튼도 잘 렌더링 되는 것을 확인하였습니다.
처음 목표였던
2️⃣ tsconfig.json, eslint, prettier등 코드 문법, 규칙, ts컴파일 등의 공통 설정 사항 공유하기
3️⃣ 범용성 높은 컴포넌트를 만들어 각 프로젝트에서 공유하기
목표를 거의 달성하였습니다. 다음 글에서는 vanilla-extract/css을 사용해 디자인 시스템 기반 재사용성 컴포넌트를 생성하는 글로 돌아오겠습니다.
https://jasonkang14.github.io/react/monorepo-with-pnpm
https://velog.io/@younyikim/Pnpm으로-Monorepo-구축하기-2.-프로젝트-구축Vite-React-TypeScript
How does pnpm work?
Ghost Depndency와의 끈질긴 싸움
React Monorepo Setup Tutorial with pnpm and Vite: React project + UI, Utils