본문 바로가기

카테고리 없음

frontend 모노레포 구성하기

모노레포를 설명하기 전에 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패키지가 설치된 것을 확인할 수 있다

pnpm install 후 client의 node_modules구조

 

이제 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;
client패키지를 실행해보자
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패키지와 동일하게 적용하면 된다

 

워크스페이스란 무엇인지, 모노레포의 특징이 무엇인지 알아보았고 최종적으로 모노레포를 구성하는 방법을 처음부터 알아보았다 회사에 입사 후 모노레포를 처음 접하게 되어 직접 경험해 보면서 이해가 되지 않았던 부분을 위주로 작성해 보았다 모노레포를 처음 접하는 사람들에게 도움이 되었길 바란다

참고자료