티스토리 뷰

728x90
SMALL

커져가는 프로젝트의 문제점 인지하기

저번 글에서 yarn berry에 대해서 학습했다. 기존 npm에 대한 숙제 하나는 끝낸 셈이다. 그런데 다른 문제가 발생했다. 개발자 대부분은 회사를 다닌다. 그리고 대부분의 회사에서는 만들고 끝이 아니라 끊임없이 유지보수하고 기능을 추가한다. 처음에는 가벼운 프로젝트라고 할지라도 시간이 지남에 따라 엄청 거대해진다. 전혀 다른 프로젝트라면 새롭게 프로젝트를 시작하면 되겠지만 기존 프로젝트의 디자인 시스템이나 유틸함수, 상수등을 공유해야될 경우가 있다. 이걸 npm으로 배포해서 여러프로젝트에서 install하는 형식도 있겠지만 관리가 너무 힘들다. 컴포넌트만 하더라도 매일매일 변경되는데 이걸 매번 배포하고 업데이트를 할 생각을 하면 벌써 머리가 아프다. 이럴때 monorepo라는 것을 도입하기도 한다.

모노레포란

멀티레포라는 개념과 상반되는 말이다. 멀티레포는 프로젝트마다 다른 레포지토리를 갖는 것을 말한다.

모노레포는 하나의 레포지토리에서 여러개의 프로젝트를 관리한다. 예를들면 공통상수, 공통 컴포넌트, 공통 커스텀훅, 공통 스타일 등등을 각각의 레포지토리로 관리하고 여러 프로젝트에서 이것들을 import해서 사용한다. 모노레포를 적용하면 개발 환경 구축이나 일관성 있는 코드 관리, 코드 재사용, 배포파이프라인 관리, 테스트코드 , 코드 리뷰 등등에서 장점이 생긴다.

yarn berry로 모노레포 구축하기

yarn berry의 장점 중 한가지는 모노레포를 지원한다는 것이다. yarn berry로 모노레포를 시작해보자. 사실 yarn berry는 그렇게 많이 사용되는 패키지 매니저가 아니고 yarn berry 모노레포는 더더욱 그렇다. 하지만 이 장점은 분명하고 그걸 알고 있기 때문에 국내의 it 대기업들에서도 yarn berry로 모노레포를 만드는 경우가 점점 생기도 있다.

기본 세팅

mkdir yarn-berry-monorepo-template
cd yarn-berry-monorepo-template
yarn set version berry
yarn init -y
yarn add -D typescript prettier
yarn dlx @yarnpkg/sdks vscode
mkdir packages

typescript, eslint, prettier는 공통으로 설정할 수 있다.

prettier는 전체를 공통으로 사용해도 큰 문제가 없기 때문에 최상위에서만 정의해주자.

.prettierrc

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 120
}

tsconfig는 사실 프로젝트마다 설정을 다르게 가져가는 경우가 많다. 하지만 공통으로 설정하는 것도 많기 때문에 extend하는 식으로 사용해주자.

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "references": [
    {
      "path": "packages/client-vite"
    },
    {
      "path": "packages/common-const"
    },
    {
      "path": "packages/common-hooks"
    }
  ],
  "exclude": ["packages/**/dist/**"],
  "include": []
}

eslint

eslint는 최상위에서 하나만 설정해도 되고 각각의 레포에서 설정해도 된다. 또한 최상위에서 여러 레포의 설정을 해줄수도 있다. 나는 그냥 각각의 레포에서 설정하기로 했다. 만약 프로젝트를 진행하다가 생각이 바뀐다면 추가하겠다. 최상위에서 설정하고 싶은 분들은 아래 첫번째 참고글을 보면 된다.

 

마음이 바뀌었다. 최상위에서 하기로 결정했다. 서버, 프론트, 공통 레포 정도를 제외하고는 대부분 린트 설정이 같은게 좋을 것 같다.

두가지만 주의하면 된다. 먼저 typescript parser와 typescript 관련 rule은 overrides해줘야한다. 또한 각각의 레포에서 각각 오버라이드를 해줘야한다. 조금 불편하긴 하지만 처음에만 세팅해놓으면 레포를 생성할 때 설정이 30초도 걸리지 않는다. 그냥 복사붙여넣기만 하면 끝이다.

 

eslintric.js

const path = require('path');

module.exports = {
  env: {
    browser: true,
    node: true,
  },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:react/recommended',
    'plugin:prettier/recommended',
    'eslint:recommended',
    'plugin:import/typescript',
    'plugin:import/recommended',
    'plugin:storybook/recommended',
  ],
  plugins: ['@typescript-eslint', 'import', 'prettier', 'react', 'react-hooks', '@emotion'],
  settings: {
    react: { version: 'detect' },
  },
  rules: {
    'prettier/prettier': 'error',
	// 생략
  },
  overrides: [
    {
      files: ['**/*.ts?(x)'],
      parser: '@typescript-eslint/parser',
      extends: [
        'airbnb-typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:@typescript-eslint/recommended-requiring-type-checking',
      ],
      parserOptions: {
        project: ['./tsconfig.json', './packages/**/tsconfig.json'],
      },
      rules: {
        '@typescript-eslint/ban-types': 'off',
        // 생략
    },
    {
      files: ['packages/client-a/**/*.ts?(x)', 'packages/client-a/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/client-a/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-components/**/*.ts?(x)', 'packages/common-components/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-components/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-const/**/*.ts?(x)', 'packages/common-const/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-const/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-components/**/*.ts?(x)', 'packages/common-components/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-components/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-const/**/*.ts?(x)', 'packages/common-const/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-const/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-hooks/**/*.ts?(x)', 'packages/common-hooks/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-hooks/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-types/**/*.ts?(x)', 'packages/common-types/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-types/tsconfig.json`),
          },
        },
      },
    },
    {
      files: ['packages/common-utils/**/*.ts?(x)', 'packages/common-utils/**/*.js?(x)'],
      settings: {
        'import/resolver': {
          typescript: {
            project: path.resolve(`${__dirname}/packages/common-utils/tsconfig.json`),
          },
        },
      },
    },
  ],
};

package.josn

{
  "name": "yarn-berry-monorepo-template",
  "devDependencies": {
    "@emotion/eslint-plugin": "^11.10.0",
    "@typescript-eslint/eslint-plugin": "^5.27.0",
    "@typescript-eslint/parser": "^5.27.0",
    "eslint": "8.2.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb-typescript": "^16.2.0",
    "eslint-config-prettier": "8.3.0",
    "eslint-import-resolver-node": "^0.3.6",
    "eslint-import-resolver-typescript": "^3.5.1",
    "eslint-plugin-import": "2.25.3",
    "eslint-plugin-jsx-a11y": "6.5.1",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-react": "7.28.0",
    "eslint-plugin-react-hooks": "4.3.0",
    "prettier": "^2.7.1",
    "typescript": "^4.8.3"
  }
}

레포지토리 생각하기

이제 공통으로 사용할 레포지토리를 만들어야한다. 내가 지금 생각했을 때는 components, const, hooks, lib, styles, types, utils 정도만 공유하면 될 것 같다. 초기에 조금 과한 투자라는 생각도 들지만 확장성을 생각했을 때는 감수해야한다고 생각한다. 그리고 프로젝트가 2개이상만 되어도 시간적으로 이득이 될 수 있다. 이제 공통으로 사용할 레포지토리를 생각했으니 만들기만 하면 된다. 또한 이런 레포지토리는 직접 사용할 일이 없기 때문에 바벨정도만 설정해주자. 바벨설정은 개인마다 다르고 모노레포와 관련이 없기 때문에 설명을 생략한다.

 

레포지토리 연결하기

root

먼저 packages라는 폴더를 만든다. 그리고 workspace를 다음과 같이 추가해준다. 폴더명이나 연결하는 방법은 마음대로 해도된다. *으로 하지 않고 직접 적어줘도 괜찮고 pacakges라는 네이밍을 변경해도 좋다. 그리고 각각의 레포로 이동해서 스크립트를 실행하는게 너무 불편하기 때문에 다음과 같은 명령어를 root에 지정해놓고 사용할 수 있다.

root - pacakge.json

{
  "name": "yarn-berry-monorepo-template",
  "packageManager": "yarn@3.2.3",
  "scripts": {
    "client-a": "yarn workspace client-a",
    "client-b": "yarn workspace client-b",
    "common-components": "yarn workspace @common/components",
    "common-const": "yarn workspace @common/const",
    "common-hooks": "yarn workspace @common/hooks",
    "common-lib": "yarn workspace @common/lib",
    "common-styles": "yarn workspace @common/styles",
    "common-types": "yarn workspace @common/types",
    "common-utils": "yarn workspace @common/utils",
    "server": "yarn workspace server"
  },
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "prettier": "^2.7.1",
    "typescript": "^4.8.3"
  }
}

각각의 레포지토리에서 name 설정하기

위에서 @common/compoenets로 접근을 했는데 접근한 폴더명은 common-componets이다. 이게 가능한 이유는 pacakge.json의 name에 매칭이 되기 때문이다. name을 신경써서 만들자

common-components - package.json

{
  "name": "@common/components",
  "packageManager": "yarn@3.2.3",
  "main": "src/index.ts",

레포지토리 import하기

다른 레포를 불러오고 싶다면 다음과 같이 import를 하면 끝이다.

client-a - package.json

  "dependencies": {
    "@common/components": "workspace:*",
    "@common/const": "workspace:*",
    "@common/hooks": "workspace:*",
    "@common/types": "workspace:*",
    "@common/utils": "workspace:*",
    "@emotion/react": "^11.10.4",
    "@emotion/styled": "^11.10.4",
    "emotion-normalize": "^11.0.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },

추가설명

package.json에서 main이라는 옵션이 있다. 만약 src를 기본 import 경로로 설정하고 싶다면 다음과 같이 설정하면 된다. yarn berry 모노레포의 특별한 설정은 아니다. npm 배포를 해본사람은 다들 아는데 따로 package.json 옵션 공부를 하지 않았거나 배포를 하지 않은 사람들을 위해서 적었다.

"main": "src/index.ts", 

cra에서는 리액트관련된 데이터를 import 할 수 없다. 예를들면 customHook을 다른 모노레포로부터 import하게되면 오류가 발생한다. 직접 설정을 하거나 vite 같은 것을 사용하면 문제없다. cra에서 바벨설정 때문에?? 그런것같은데 나는 cra를 사용하지 않아서 패스한다.

깃헙링크

https://github.com/yoonminsang/yarn-berry-monorepo-template/tree/7951ffd82a7b6a61ac2f47d1f34e006def6a9a1a

참고글

https://techblog.woowahan.com/7976/

https://d2.naver.com/helloworld/0923884

728x90
LIST
댓글
공지사항