OTTER-LOG

lerna monorepo 환경설정

lerna monorepo 환경설정
by otter2022년 12월 18일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

모노레포

(Ref: 모던 프론트엔드 프로젝트 구성 기법 - 모노레포 개념 편 )

위키 백과에 따르면 모노 레포는 버전 관리 시스템에서 두 개 이상의 프로젝트 코드가 동일한 저장소에 저장되는 소프트웨어 개발 전략이라고 설명 된다.

하나의 저장소를 사용하지만, 개별적인 프로젝트로서 존재하게 된다. 또한, 개별 프로젝트 사이에 의존성이 존재할 수 있다. 예를 들면 A 패키지가 B 패키지로의 의존성을 가질 수 있지만 이 둘은 한 레포지토리에 존재한다. 심지어, NPM 등의 패키지에 배포를 하지않아도 두 패키지를 연결해 사용할 수 있다.

모노레포의 장점

  • 더 쉬운 프로젝트 생성

    하나의 저장소만 사용하며, ci-cd 공통적으로 적용할

    린트나, 프리티어 룰도 일률적으로 설정

  • 더 쉬운 의존성 관리

    npm 에 퍼블리시 하지 않고도, 같은 저장소에 있으므로 불러와 사용할 수 있음.

모노레포의 단점

  • 무분별한 의존성 연결 가능

    의존성 연결이 쉽기 때문에 오히려 과도한 의존 관계가 나타날 수 있음.

  • CI 속도 저하

    레포지토리 크기 자체가 커지기 때문에, 레포지토리 훅을 기반으로 동작하는 도구들의 속도 저하

체크 리스트

이번 프로젝트는 리액트 UI 라이브러리를 만들기 위함이였으므로, 아래의 체크리스트를 작성하고 진행했다.

  1. 모노레포 패키지를 구성해야한다.
  2. react, ts, jest 는 공통으로 사용할 수 있어야 한다.
  3. 배포하기 전, 배포할 패키지를 테스트할 수 있는 환경이 있어야 한다.
    • 이를 위해 symlink 를 연결해야한다.
  4. 버전 관리가 쉽게 이루어질 수 있어야 한다.
  5. 패키지를 자동 배포할 수 있는 환경이 구축되어야 한다.

lerna

lerna 설치하기

npx lerna init --independent
// root package.json { "name": "root", "private": true, "workspaces": [ "packages/*" ], "devDependencies": { "lerna": "^6.0.3" } }
// lerna.json { "useWorkspaces": true, "version": "independent", // 버전 관리를 하위 모듈마다 별개로 하기 위해 independent를 적용 // 버전 관리를 한꺼번에 하고 싶다면, 여기에 버전을 명시해도 된다. }

공통적으로 사용할 디펜던시

루트 경로에는, 패키지 내부에 있는 워크스페이스들이 공통적으로 사용할 디펜던시를 적용 할 수 있다. 이번 환경설정은 리액트 컴포넌트 라이브러리를 만드는 것이 목적이었기 때문에, 이전 글에서 rollup 을 이용해 react, ts, jest 설정을 마무리 했었다. 이를 제외한 lint, prettier 설정을 마무리 하자.

eslint, prettier

yarn add -DW eslint prettier // dev디펜던시로 하기위해 D, yarn workwpace가 아닌 루트에 제공해기 위해 W 커맨드를 추가
// .eslintrc.js module.exports = { "env": { "browser": true, "es2021": true }, "extends": [ "plugin:react/recommended", "standard-with-typescript" ], "overrides": [ ], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": [ "react" ], "rules": { } } // 일반적으로 사용하는 eslint를 사용
// .prettierrc { "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "quoteProps": "as-needed", "jsxSingleQuote": false, "trailingComma": "es5", "arrowParens": "always", "endOfLine": "lf", "bracketSpacing": true, "jsxBracketSameLine": false, "requirePragma": false, "insertPragma": false, "proseWrap": "preserve", "vueIndentScriptAndStyle": false }

루트 경로에 린트와 프리티어를 적용함을 통해 다른 패키지들에도 동일하게 적용된다.


workspace 만들기

lerna create over-test

그리고, 둘의 경로에 package.json 을 조금 수정을 진행했다. 조금 일반적인 네이밍을 사용하기 위함이었다.

// over-test package.json { "name": "over-test", "version": "1.0.0", ... "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", // 각각 빌드될때의 타입 경로 ... "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, // react component 라이브러리로 사용하기 위함 "scripts": { "build": "yarn clean && yarn build:typings && rollup -c ../../rollup.config.mjs", // index.d.ts 를 build할때 같이 생성합니다. "watch": "rollup -c ../../rollup.config.mjs -w", "build:typings": "tsc -p ../../tsconfig.json --declarationDir dist", "clean": "rm -rf dist" } // 여기에 있는 경로는 공통으로 사용할 rollup.config, tsconfig를 사용할 경로를 적어주었습니다. }

TypeScript 를 기반으로 한다면, build 될 때의 index.d.ts 생성을 신경써야한다. 그렇지 않다면 배포된 라이브러리를 타입스크립트를 기반으로 사용할때에 타입오류가 발생하기 때문이다.


playground

패키지를 배포하기 전에, 패키지가 배포되면 잘 작동되는지 확인하는 작업공간이 필요하다고 생각했다. 모든 부분을 NPM에 배포후 테스트 하는 것은 비효율적이라 생각했기 때문이다. 또, 이러한 부분이 모노레포를 사용하는 장점이기도 하니까. 그리고, 이 부분을 통해 나중에 이쁜 API 문서를 만들어봐도 좋을 것 같다라는 생각도 했다.

일단 이부분을 create-next-app --ts 환경으로 구성했고, 버전관리는 필요하지 않다고 생각했다. 아직 사이트를 만들지는 결정하지 않았으니까 private 설정을 true 로 진행해 배포되지 않도록 설정하고 version 은 제거했다.

"name": "playground", "private": true, ... // version은 삭제했습니다.

symlink 는 링크를 연결해 연결해 원본 파일을 직접 사용하는 것과 같은 효과를 내는 링크를 말한다.

playground 를 만든 이유는, npm 에 해당 컴포넌트를 배포하기 전 작동이 잘 되는지 확인하기 위함이었고 이를 실시간으로 확인하기 위해 symlink 를 연결해야 했다. Lerna + yarn 조합이라면 두가지 방법으로 진행할 수 있다.

첫번째 방법은 leranbooststrap --hoist 명령어를 통해 심링크를 연결는 것이다. 이 명령어는 모든 노드 뮤듈을 root 로 모아 진행된다.

lerna add <종속성으로 설치할 패키지 이름> --scope=playground lerna add over-test --scope=playground

그리고 이 패키지를 playground 에 연결해 사용한다.

심링크가 연결이 잘 되었는지 확인하기 위해, rollup 의 build watch로 이를 확인해 보자. over-testwatch 모드를 실행시키고, playgrounddev 환경으로 실행시킨 후 수정내용이 실시간으로 반영되는지 확인할 수 있다.

변경 사항은 아주 잘 적용되지만, 타입스크립트 빌드를 자동으로 진행하지 못한다. (여기는 rollup build watch니까) 타입스크립트의 변경사항이 필요할때는 다시 빌드해주어야 하는 문제점이 있다.

두번째 방법은 yarn workspace 를 사용하는 방법이다. yarn workspace 는 다음과 같이 패키지 제이슨에 직접 적어주는 방식으로 쉽게 적용할 수 있다.

// playground package.json "dependencies": { "@over-test": "workspace:*", // yarn workspace를 통해, 다른 workspace에 존재하는 // 디펜던시를 불러올 수 있습니다. "next": "13.0.4", "react": "18.2.0", "react-dom": "18.2.0" },

결과적으로 선택한 방법은 후자였다. 팀원과 함께 사용하기 위한 환경을 구성하는 중이었으므로, 쉬운방법이 좋다 판단했다. 그리고, 레퍼런스에 따르면 yarn workspace 가 루트의 노드모듈의 중복을 관리해주어 더 효율적이라 한 점도 결정에 영향을 끼쳤다.

commitlint

모노레포를 사용한다면, 보통 다른 개발자와 함께 사용하기 위함일 것이다. 이를 위해, 커밋 컨벤션을 정해 어떠한 커밋을 했는지 확인할 수 있지만 commitlint 를 적용한다면 실수를 미연에 방지할 수 있다고 생각했다. 사실 사람은 언제나 실수할 수 있으므로, 커밋 컨벤션을 못지키는 경우가 존재한다. 물론 해당 부분을 수정할 수 있지만 수정하는 것도 귀찮으니까. 커밋단계에서 커밋 컨벤션의 오류를 파악해주는 느낌으로 파악했다.

yarn add -DW @commitlint/{cli,config-conventional}
// commitlint.config.js module.exports = { extends: ['@commitlint/config-conventional'], };
yarn add -DW husky npx husky add .husky/commit-msg 'npx commitlint --edit $1' // husky에 commitlint를 적용합니다.

이제, conventional-commit 만을 사용해야 한다.

잘못된 커밋메세지를 입력하면, 위의 경우 foo: test 를 했을 경우 오류가 나고 commit이 되지 않는다.

change-log

이번에 모노레포를 구성하는 이유는 리액트 UI 라이브러리를 만들기 위함이였으므로, 버전관리에 대해서도 신경쓰고자 했다. 결과적으로는 lernachange-log 를 이용하는 방법으로 진행했다. 우선, 개별적인 패키지를 매번 직접 바꿔주는 방법은 때로는 무질서한 버전닝을 그리고 귀찮다는 문제점이 발생한다.

// lerna.json { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, "version": "independent", "npmClient": "yarn", "command": { "version": { "conventionalCommits": true, "changelogPreset": { "name": "conventional-changelog-conventionalcommits", "types": [ { "type": "feat", "section": ":rocket: New Features", "hidden": false }, { "type": "fix", "section": ":bug: Bug Fix", "hidden": false }, { "type": "docs", "section": ":memo: Documentation", "hidden": false }, { "type": "style", "section": ":sparkles: Styling", "hidden": false }, { "type": "refactor", "section": ":house: Code Refactoring", "hidden": false }, { "type": "build", "section": ":hammer: Build System", "hidden": false }, { "type": "chore", "section": ":mega: Other", "hidden": false } ] } }, "publish": { "conventionalCommits": true } } }

(ref: https://kdydesign.github.io/2020/10/19/open-source-flow/ )

위처럼 lenra version 에도, 커밋 린트를 적용한 뒤 커밋과 푸쉬를 하고 lerna version 을 입력하면 오류가 나는데 publish 관련 메세지를 적어주지 않아서 그렇다.

"command": { "version": { "ignoreChanges": ["**/*.md", "packages/playground"], // playground는 lerna 버전에 포함되지 않아서, 무시해야 함. "message": "chore(release): publish", // version에 대한 커밋메세지 // 자동으로 version을 진행하고, publish를 할 것이므로 publish라는 메세지를 사용 ....

배포 자동화

그런데, 매번 main 브랜치로 올리고 이에 대한 작업 내용을 개별적으로 publish 하는 것은 번거로운 작업이 될 것이다. 이를 위해 github action 을 통해 배포자동화를 진행하자. 배포와 관련된 부분은 lerna 에 있는 publish 커맨드를 통해 쉽게 적용할 수 있다.

name: Publish on: push: branches: - main jobs: publish: runs-on: ubuntu-latest steps: - name: "Checkout" uses: actions/checkout@v2 with: fetch-depth: 0 - name: "Use NodeJS 14" uses: actions/setup-node@v2 with: node-version: "14" - name: "Connect to NPM" run: | npm config set @over-ui:registry https://registry.npmjs.org/ npm config set registry=https://registry.npmjs.org/ npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} npm whoami // 일반적으로 main에 배포를 진행할 때에 위처럼 npm에 연결 후 // lenra publish를 진행한다. // 이룰 활용해 action에서 연결을 진행합니다. - name: "Connect to git" run: | git config user.name "otterp012" git config user.email "otterp012N@users.noreply.github.com" - name: "Install Dependencies" run: yarn install - name: "Test" run: yarn test - name: "Version & add Change-log" run: npx lerna version --no-private --yes // 이 부분을 통해 version 또한 자동으로 진행한다. - name: "Build" run: yarn build - name: "Publish" run: npx lerna publish from-git --no-private --yes // from-git 커맨드를 통해 현재 `git`에 올라와 있는 패키지 중, version이 // 달라진 부분만을 배포한다.

위처럼 gtihub action 을 적용하면, main 브랜치에 push 가 될 경우 자동으로 test 가 진행된 후 lerna versionlerna publish 가 진행되어 달라진 패키지의 버저닝이 완료되고 npm에 publish 가 자동으로 진행된다.


stroybook 적용하기

컴포넌트 라이브러리의 문서화를 고민하던 중 storybook 을 이용하는 방법이 가장 유용해 보여 storybook 을 적용하기로 했다. 그래서, 모노레포에 스토리북 설정을 추가해야했다.

npx sb init // 스토리북을 설치 yarn add -DW @storybook/addon-docs @storybook/addon-a11y // mdx파일을 사용하기 위해, addon-docs를 // 접근성을 테스트하기 위해 addon-a11y를 설치 // 'storybook-addon-react-docgen', // 'react-docgen-typescript', // 플러그인을 통해 추가적으로 손쉽게 props table을 만들 수 있지만 // 컴포넌트의 displayName과 선언된 이름이 다르면 제대로 출력되지 않는 버그가 있어 사용하지 못했다. // 일반적으로 위를 사용하면, 컴포넌트의 프랍들을 쉽게 추출할 수 있다.

그리고, 디렉토리 구조에서 아래처럼 스토리를 각 패키지 안에서 작성하고 싶었기 때문에 추가적인 설정을 진행해 주었다.

package1 --- stories.tsx package2 --- strries.tsx

이러한 storeis 의 경로 문제를 해결하고 두가지 addon 을 적용하기 위해 .storybookmain.js 파일을 수정

// main.js module.exports = { stories: ['../packages/**/src/*.stories.@(mdx|tsx)'], addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-docs', '@storybook/addon-a11y', ], framework: '@storybook/react', core: { builder: '@storybook/builder-webpack5', }, };

그런데, lerna 에서 src 내용이 달라지는 부분을 기점으로 삼아 버전이 업그레이드 되고 있다. 이러한 상황에서 stories 의 추가나 변경 삭제로 인해 버전닝이 진행되지 않도록 lerna 의 설정 해 주어야 한다.

"command": { "version": { "ignoreChanges": ["**/*.md", "playground/**", "*.test.tsx", "*.stories.tsx", "*.stories.mdx" ], // 이 부분의 변경사항은 무시됩니다.

Ref