velog
graduation
Monorepo

Intro

인턴십 기간 동안 처음으로 단일 레포지토리에 많은 파일들과 여러 프로젝트가 포함된 구조를 접했었다. 당시에는 모노레포(Monorepo)라는 개념조차 생소했지만, 모노레포가 제공하는 장점을 알게 되면서, 현재 개선 중인 졸업 프로젝트에도 해당 방식을 적용하고 싶어졌다.

Monorepo란?

하나의 저장소를 의미하는 Monolithic Repository의 줄임말로, 여러 프로젝트를 단일 레포지토리에서 관리하는 방식이다.

현대 개발 환경에서 웹 애플리케이션이 제공하는 여러 프로젝트와 라이브러리가 서로 의존적으로 작동하는 경우가 많아지고 있다고 한다.

모노레포를 사용함으로써 코드의 일관성을 유지할 수 있고, 변경 사항이 발생하여도 비교적 쉽게 수정할 수 있는 용이성으로 최근 많이 도입하고 있는 방식이라고 한다.

프로젝트 도입 과정

모노레포 구성을 하는 방법으로 Yarn, Lerna, Nx, Turborepo가 있었다.

그 중, Nx와 Turborepo의 경우 캐싱 기능을 지원하여 빌드 측면에서 우수하고, 의존성 그래프 시각화 기능도 제공하는 등 좀 더 지원하는 기능들이 많다고 하였다.

하지만, 이번 프로젝트에서는 공통 컴포넌트의 재사용에 초점을 맞췄고, 새로운 기술적 시도에 의의를 두며 Yarn Workspaces를 선택하게 되었다.

1. yarn berry 설정

Yarn의 최신 버전인 Berry를 사용하고자 다음 명령어를 입력한다.

yarn set version berry

2. .yarnrc.yml

Zero-Install의 기능을 최대한 활용하고자 nodeLinker를 pnp로 설정하였다. node_modules를 사용하지 않고 모듈의 설치와 관리를 효율적으로 만드는 이점이 있다.

yarnPath: .yarn/releases/yarn-4.1.1cjs
nodeLinker: pnp

nodeLinker를 pnp (Plug'n'Play)로 설정하면 다음과 같이 .pnp.cjs와 .pnp.loader.mjs 두 파일이 생성된다.

.pnp.cjs

CommonJS 모듈 형식으로, Yarn PNP가 패키지를 해석하고 찾는 과정에 대한 정보를 포함하고 있다.

#!/usr/bin/env node
/* eslint-disable */
"use strict";

const RAW_RUNTIME_STATE =
'{\
  "__info": [\
    "This file is automatically generated. Do not touch it, or risk",\
    "your modifications being lost."\
  ],\
  "dependencyTreeRoots": [\
    {\
      "name": "tripplannerz-frontend",\
      "reference": "workspace:."\
    },\
    {\
      "name": "react-vite",\
      "reference": "workspace:apps/react-vite"\
    },\
    {\
      "name": "@tripplannerz/e2e",\
      "reference": "workspace:shared/e2e"\
    },\
    {\
      "name": "@tripplannerz/kakao",\
      "reference": "workspace:shared/kakao"\
    },\
    {\
      "name": "@tripplannerz/ui",\
      "reference": "workspace:shared/ui"\
    }\
  ],\
  
  //이하 생략

이 프로젝트에서는 react-vite 라는 주 애플리케이션과, E2E테스트를 수행하는 e2e, Kakao Map SDK를 이용한 지도 렌더링 컴포넌트 kakao, 그리고 UI 컴포넌트 및 StoryBook을 작성한 ui 공유 모듈을 구성하였다.

.pnp.loader.mjs

ES 모듈로 작성된 프로젝트에서 사용되며, Yarn PnP를 지원하는 Node.js 환경에서 ES모듈을 사용할 때 필요한 정보를 제공한다.

3. package.json 설정하기

.
├── apps/
│   └── react-vite/
│       └── package.json // 메인 웹 애플리케이션의 구성 package.json
├── shared/
│   ├── e2e/
│   │   └── package.json // E2E(End-to-End) 테스트를 위한 설정 package.json
│   ├── kakao/
│   │   └── package.json // Kakao Map SDK를 활용한 지도 렌더링 컴포넌트 package.json
│   └── ui/
│       └── package.json // UI 컴포넌트 라이브러리의 package.json
└── package.json // 루트 디렉토리 package.json, 프로젝트의 워크스페이스 구조와 공통 의존성을 정의

workspaces 필드를 사용하여 패키지의 위치들을 지정하였다.

apps에는 실제 애플리케이션의 패키지들을 보관하고, shared에는 애플리케이션에 사용될 공통 컴포넌트에 대한 패키지들을 보관한다.

{
  "name": "tripplannerz-frontend",
  "packageManager": "yarn@4.1.1",
  "private": true,
  "workspaces": {
    "packages": [
      "apps/*",
      "shared/*"
    ]
  },
 
 // 이하 생략

4. TypeScript 설정하기

tsconifg.base.json

{
    "compilerOptions": {
        "target": "ESNext",
        "useDefineForClassFields": true,
        "lib": ["DOM", "DOM.Iterable","ESNext"],
        "allowJs": false,
        "skipLibCheck": false,
        "esModuleInterop": false,
        "allowSyntheticDefaultImports": true,
        "allowImportingTsExtensions": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "ESNext",
        "moduleResolution": "Node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react"
    }
}

apps/react-vite/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "types": ["vite/client"],
    "paths": {
      "@/*": ["src/*"]
    },
  },
  "include": ["./src"]
}

5. ESLint, Prettier 설정하기

.eslintrc.js

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh', '@typescript-eslint', 'prettier'],

  rules: {
    'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
    'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto',
      },
    ],
    '@typescript-eslint/no-explicit-any': 'off',
  },
};

.prettierrc

{
    "arrowParens": "always",
    "bracketSameLine": false,
    "bracketSpacing": true,
    "embeddedLanguageFormatting": "auto",
    "htmlWhitespaceSensitivity": "css",
    "insertPragma": false,
    "jsxSingleQuote": true,
    "printWidth": 100,
    "proseWrap": "always",
    "quoteProps": "as-needed",
    "requirePragma": false,
    "semi": true,
    "singleAttributePerLine": false,
    "singleQuote": true,
    "tabWidth": 2,
    "trailingComma": "es5",
    "useTabs": false,
    "vueIndentScriptAndStyle": false
}

6. Kakao Map 컴포넌트 관련 프로젝트 분할

Kakao Map SDK를 활용해 지도를 렌더링하는 컴포넌트의 경우 공통 컴포넌트로 생각이 들어 모노레포의 특징을 활용한 분할을 시도했다.

결과

이번 프로젝트에서 yarn workspace를 활용한 모노레포로 변경 중인 루트 디렉토리의 package.json 파일이다.

{
  "name": "tripplannerz-frontend",
  "packageManager": "yarn@4.1.1",
  "private": true,
  "workspaces": {
    "packages": [
      "apps/*",
      "shared/*"
    ]
  },
  "scripts": {
    "dev:react-vite": "yarn workspace react-vite dev",
    "build:react-vite": "yarn workspace react-vite build",
    "storybook": "yarn workspace @tripplannerz/ui storybook",
    "e2e": "yarn workspace @tripplannerz/e2e test:dev"
  },
  "lint-staged": {
    "**/*.{js,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^7.2.0",
    "@typescript-eslint/parser": "^7.2.0",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react": "^7.34.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "husky": "^9.0.11",
    "lint-staged": "^15.2.2",
    "prettier": "^3.2.5",
    "typescript": "^5.4.2"
  }
}

모노레포를 구축하면서 느낀 점

모노레포는 여러 프로젝트를 하나의 레포지토리에서 관리하는 개발 방법으로써, 대규모 프로젝트나 복잡한 의존성 구조를 가진 애플리케이션에 적합하다고 한다. 그러다보니, 상대적으로 규모가 크지 않은 졸업 프로젝트에 적용을 하면서도 과도한 시도라고 생각했었다.

그러나, 직접 구축을 해보면서 모노레포의 개념과 구조에 좀 더 익숙해질 수 있었다. 추후에 더 복잡한 프로젝트 환경에 직면했을 때, 유연하게 대처할 수 있는 능력을 키우는 데 도움이 될 것 같다.

참고 문헌

yarn 공식 블로그 (opens in a new tab)

모노레포에 대해 알아보자 (opens in a new tab)

모던 프론트엔드 프로젝트 구성 기법 - 모노레포 도구 편 (opens in a new tab)