lerna monorepo 환경설정
모노레포
(Ref: 모던 프론트엔드 프로젝트 구성 기법 - 모노레포 개념 편 )
위키 백과에 따르면 모노 레포는 버전 관리 시스템에서 두 개 이상의 프로젝트 코드가 동일한 저장소에 저장되는 소프트웨어 개발 전략이라고 설명 된다.
하나의 저장소를 사용하지만, 개별적인 프로젝트로서 존재하게 된다. 또한, 개별 프로젝트 사이에 의존성이 존재할 수 있다. 예를 들면 A
패키지가 B
패키지로의 의존성을 가질 수 있지만 이 둘은 한 레포지토리에 존재한다. 심지어, NPM
등의 패키지에 배포를 하지않아도 두 패키지를 연결해 사용할 수 있다.
모노레포의 장점
-
더 쉬운 프로젝트 생성
하나의 저장소만 사용하며,
ci-cd
공통적으로 적용할린트나, 프리티어 룰도 일률적으로 설정
-
더 쉬운 의존성 관리
npm
에 퍼블리시 하지 않고도, 같은 저장소에 있으므로 불러와 사용할 수 있음.
모노레포의 단점
-
무분별한 의존성 연결 가능
의존성 연결이 쉽기 때문에 오히려 과도한 의존 관계가 나타날 수 있음.
-
CI 속도 저하
레포지토리 크기 자체가 커지기 때문에, 레포지토리 훅을 기반으로 동작하는 도구들의 속도 저하
체크 리스트
이번 프로젝트는 리액트 UI 라이브러리를 만들기 위함이였으므로, 아래의 체크리스트를 작성하고 진행했다.
- 모노레포 패키지를 구성해야한다.
react, ts, jest
는 공통으로 사용할 수 있어야 한다.- 이 부분은, 여기에서 작성했다.
- 배포하기 전, 배포할 패키지를 테스트할 수 있는 환경이 있어야 한다.
- 이를 위해
symlink
를 연결해야한다.
- 이를 위해
- 버전 관리가 쉽게 이루어질 수 있어야 한다.
- 패키지를 자동 배포할 수 있는 환경이 구축되어야 한다.
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 연결하기
symlink
는 링크를 연결해 연결해 원본 파일을 직접 사용하는 것과 같은 효과를 내는 링크를 말한다.
playground
를 만든 이유는, npm
에 해당 컴포넌트를 배포하기 전 작동이 잘 되는지 확인하기 위함이었고 이를 실시간으로 확인하기 위해 symlink
를 연결해야 했다. Lerna + yarn
조합이라면 두가지 방법으로 진행할 수 있다.
첫번째 방법은 leran
의 booststrap --hoist
명령어를 통해 심링크를 연결는 것이다. 이 명령어는 모든 노드 뮤듈을 root
로 모아 진행된다.
lerna add <종속성으로 설치할 패키지 이름> --scope=playground lerna add over-test --scope=playground
그리고 이 패키지를 playground
에 연결해 사용한다.
심링크가 연결이 잘 되었는지 확인하기 위해, rollup
의 build watch로 이를 확인해 보자. over-test
의 watch
모드를 실행시키고, playground
는 dev
환경으로 실행시킨 후 수정내용이 실시간으로 반영되는지 확인할 수 있다.
변경 사항은 아주 잘 적용되지만, 타입스크립트 빌드를 자동으로 진행하지 못한다. (여기는 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 라이브러리를 만들기 위함이였으므로, 버전관리에 대해서도 신경쓰고자 했다. 결과적으로는 lerna
의 change-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 version
과 lerna 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
을 적용하기 위해 .storybook
의 main.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" ], // 이 부분의 변경사항은 무시됩니다.