lerna monorepo 환경설정
모노레포
(아래 내용은 모던 프론트엔드 프로젝트 구성 기법 - 모노레포 개념 편 의 내용을 정리한 것입니다.)
위키 백과에 따르면 모노 레포는 버전 관리 시스템에서 두 개 이상의 프로젝트 코드가 동일한 저장소에 저장되는 소프트웨어 개발 전략이라고 설명됩니다.
하나의 저장소를 사용하지만, 개별적인 프로젝트로서 존재합니다. 또한 개별 프로젝트 사이에 의존성이 존재하거나 같은 제품군이거나 하는 정의된 관계가 존재합니다.
모노레포를 사용하게 되면 다음과 같은 이점을 가질 수 있습니다.
-
더 쉬운 프로젝트 생성
하나의 저장소만 사용하며,
ci-cd
전략도 공통적으로 적용할 수 있습니다.린트나, 프리티어 룰도 일률적으로 설정할 수 있습니다.
-
더 쉬운 의존성 관리
npm
에 퍼블리시 하지 않고도, 같은 저장소에 있으므로 불러와 사용할 수 있습니다.
체크 리스트
- 모노레포 패키지를 구성해야한다.
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
설정을 마무리 했고 이어 아래와 같은 eslint, pretteir
설정을 진행했습니다.
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
을 조금 수정합시다. 폴더명 또한 lib
에서 src
로 수정해 줍시다.
물론 이 부분은 개인의 취향이 반영되는 부분이지만 저는 일반적은 이름을 선택했습니다.
// 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", "watch": "rollup -c ../../rollup.config.mjs -w", "build:typings": "tsc -p ../../tsconfig.json --declarationDir dist", "clean": "rm -rf dist" } // 여기에 있는 경로는 공통으로 사용할 rollup.config, tsconfig를 사용할 경로를 적어주었습니다. }
이제, yarn build
또는 lerna build
를 하시게 되면 src
에 작성한 ts
파일의 빌드 결과물을 확인하실 수 있습니다. 또, 배포를 위해 index.d.ts
파일을 꼭 만들어 주셔야 합니다! 저는 scripts
에 위와 같이 적용해 두었습니다.
playground
패키지를 배포하기 전에, 패키지가 배포되면 잘 작동되는지 확인하는 작업공간이 필요하다고 생각했습니다. 이러한 공간이 레포에 존재하지 않으면 우리는 무조건 npm에 배포를 하고 확인을 해야할텐데 이러한 과정이 번거롭다고 생각했기 때문입니다. 또한, 이 부분에 usage
를 적용해 둔다면 추후에 README
를 적용하기 편리할 것이라 생각했습니다.
그리고, 저는 이부분을 create-next-app --ts
환경으로 구성했습니다. 이외에 따로 playground
에 적용한 속성은 없었습니다. 다만, 배포할 배키지가 아니며 버전을 관리하지 않을 예정입니다. 그래서, package.json
을 아래와 같이 수정했습니다.
"name": "playground", "private": true, ... // version은 삭제했습니다.
symlink 연결하기
playground
를 만든 이유는, npm
에 해당 컴포넌트를 배포하기 전 작동이 잘 되는지 확인하기 위함이었습니다. 이를 위해서, 그리고 실시간으로 반영하기 위해서 우리는 symlink
를 연결해야 합니다. lerna
의 bootstrap --hoist
명령어를 통해 root
로 전체를 연결할 수도 있고 아래와 같이 개별적으로 연결해줄 수도 있습니다.
lerna add <종속성으로 설치할 패키지 이름> --scope=playground lerna add over-test --scope=playground
playground
의 종속성에 over-test
가 들어갑니다.
그리고, over-test
의 rollup watch
설정을 해줍시다. over-test
가 매번 내용이 달라질때마다 바로 빌드됩니다.
// over-test, package.json ... "scripts": { "build": "yarn clean && yarn build:typings && rollup -c ../../rollup.config.mjs", "watch": "rollup -c ../../rollup.config.mjs -w", "build:typings": "tsc -p ../../tsconfig.json --declarationDir dist", "clean": "rm -rf dist" }
over-test
의 watch
모드를 실행시키고, playground
는 dev
환경으로 실행시킨 후 수정내용이 실시간으로 반영되는지 확인해 보았습니다.
변경사항이 잘 반영됩니다. 다만 ts
의 경우 rollup
설정으로 마무리 되지 않아, 바뀔때마다 다시 빌드해야 하는 단점이 있습니다.
위와 같은 방법도 존재하지만, 아래와 같이 상대적으로 쉽게 적용할 수 있는 방법이 존재했습니다. 모노레포를 저 혼자 사용하는 것이 아닌, 다른 개발자와 함께 사용한다는 것을 염두에 두었기에 조금 더 쉬운 사용법을 적용하는 것이 좋다고 생각했습니다.
// playground package.json "dependencies": { "@over-test": "*", // yarn workspace를 통해, 다른 workspace에 존재하는 // 디펜던시를 불러올 수 있습니다. "next": "13.0.4", "react": "18.2.0", "react-dom": "18.2.0" },
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 라이브러리를 만들기 위함이며, 이에 대한 버전의 관리가 필요합니다. 버전의 관리가 개별적으로 이루어진다면 때때로 버전의 증가가 사람들마다 생각하는 부분이 다를 수 있습니다. 위의 commitlint
를 chagne-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
관련 메세지가 존재하지 않아서 그렇습니다. lerna.json
을 다음 내용을 추가해줍시다.
"command": { "version": { "ignoreChanges": ["**/*.md", "packages/playground"], // playground는 lerna 버전에 포함되지 않습니다. "message": "chore(release): publish", // version에 대한 커밋메세지는 위를 사용합니다. // 자동으로 version을 진행하고, publish를 할 것이므로 publish라는 메세지를 사용했습니다. ....
이제 change log
가 잘 생성되는 것을 확인할 수 있습니다.
배포 자동화
그런데, 매번 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" ], // 이 부분의 변경사항은 무시됩니다.
저는 mdx
파일을 사용해 componet
설명 및 개발 문서를 통합할 계획이어서 마지막으로 다음 타입을 설치해주고 마무리 했습니다.
yarn add -DW @types/mdx // mdx 타입선언을 추가합니다.