Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] client, designsystem 간 모노레포 의존성 설정 #532

Closed
wants to merge 12 commits into from

Conversation

Todari
Copy link
Contributor

@Todari Todari commented Aug 28, 2024

참고사항

workflow 내용을 담은 PR을 main을 향하도록 따로 만들도록 하겠습니다~

issue

구현 목적

  • 생산성 향상을 위해 design system을 도입하였으나, npm 배포가 되어야지만 client 코드에서 이를 적용할 수 있는 수직적 업무방식이 오히려 생산성을 저하시키는 문제가 있습니다.
  • 두가지 패키지를 모노레포 툴을 이용하여 관리함으로써 패키지 관리 및 의존성 관리를 간편하게 하여 생산성을 향상시키고자 합니다.

생산성 관리 툴

Yarn Workspace

장점

간편한 설정: Yarn Workspaces는 설정이 간단하며, 별도의 복잡한 설정 없이 모노레포에서 여러 패키지를 관리할 수 있습니다.

의존성 관리: 모든 패키지의 의존성을 루트에서 한 번에 설치하고, 중복된 패키지를 공유하여 디스크 공간을 절약하고 설치 속도를 향상시킬 수 있습니다.

디스크 공간 절약: 중복되는 패키지를 루트에서 공유하여 설치하므로, 전체 프로젝트의 디스크 사용량이 줄어듭니다.

빠른 설치: 패키지를 한 번에 설치하기 때문에 설치 속도가 빠르며, 의존성 문제를 최소화할 수 있습니다.

단점

제한된 기능: Yarn Workspaces는 기본적인 모노레포 기능만 제공하므로, 복잡한 빌드 시스템이나 작업 그래프 분석, 빌드 캐시 등 고급 기능이 부족합니다.

추가 도구 필요: 고급 기능이나 대규모 프로젝트에서 필요한 기능을 위해서는 Lerna나 Nx 같은 추가 도구와 함께 사용해야 할 수 있습니다.

독립적인 버전 관리 부재: Yarn Workspaces 자체로는 패키지의 버전을 독립적으로 관리할 수 있는 기능이 없습니다. Lerna와 함께 사용하여 이러한 기능을 추가해야 합니다.

Nx

장점

빌드 캐시 및 작업 그래프: Nx는 작업 간의 의존성을 분석하고 빌드, 테스트, 린트 작업의 결과를 캐시하여 빌드 시간을 크게 단축할 수 있습니다.

다양한 플러그인 지원: Nx는 React, Angular, NestJS 등 다양한 프레임워크와의 통합을 위한 플러그인을 제공하여, 빠르고 일관된 개발 환경 설정을 가능하게 합니다.

대규모 프로젝트에 최적화: Nx는 대규모 모노레포에서 발생하는 복잡한 워크플로우를 관리하는 데 최적화되어 있습니다. 특히 여러 팀이 협력하는 환경에서 유용합니다.

유지 관리 도구: Nx는 코드 스캐폴딩, 린팅, 테스트 설정 등 프로젝트 유지 관리를 위한 강력한 도구를 제공합니다.

단점

복잡도: Nx는 매우 강력한 도구이지만, 그만큼 설정과 사용법이 복잡할 수 있습니다. 작은 프로젝트에서는 필요 이상으로 복잡하게 느껴질 수 있습니다.

학습 곡선: Nx의 다양한 기능과 설정을 이해하고 사용하는 데 시간이 걸릴 수 있습니다. 특히 처음 사용하는 개발자에게는 다소 어렵게 느껴질 수 있습니다.

비교적 새로운 도구: Lerna와 비교했을 때, Nx는 상대적으로 새로운 도구이기 때문에 일부 개발자나 팀에서 경험이 부족할 수 있습니다.

Lerna

장점

독립적인 버전 관리: 각 패키지의 버전을 독립적으로 관리할 수 있어, 특정 패키지만 업데이트할 수 있습니다.

퍼블리시 기능: 여러 패키지를 한 번에 퍼블리시하거나 선택적으로 퍼블리시하는 기능이 강력합니다.

NPM/Yarn과의 호환성: Lerna는 기존의 NPM 또는 Yarn 기반 프로젝트와 쉽게 통합할 수 있습니다. 기존 프로젝트에 모노레포 관리 기능을 추가하기에 적합합니다.

유연성: Lerna는 다양한 워크플로우와 설정에 대해 유연하게 작동할 수 있으며, 특정 사용 사례에 맞게 커스터마이징할 수 있습니다.

단점

빌드 성능: Lerna 자체는 빌드 성능 최적화에 대한 기능이 부족합니다. 빌드 캐시나 작업 그래프 분석과 같은 기능이 없어 대규모 프로젝트에서는 빌드 시간이 길어질 수 있습니다.

확장성 한계: 아주 큰 규모의 모노레포에서는 Lerna의 기본적인 기능만으로는 관리가 어려울 수 있으며, 추가적인 도구와의 통합이 필요할 수 있습니다.

결론

  • 우리 프로젝트에서는 패키지 관리로 npm 을 사용하고, 큰 규모가 아닌 2개의 패키지가 있는 소규모입니다.
  • 디자인 시스템에 대한 퍼블리싱, 배포 자동화를 통해 배포의 복잡성을 줄여줄 수 있습니다.
  • 위 이유들과 러닝커브가 적당하며 참고할 자료가 많은 Lerna를 선택하고자 합니다.

구현 사항

1. 모노레포 환경을 위한 폴더 구조 변경

/2024-haengdong
├── server
├── client
│   └── src
└── HDesign
    └── src

기존에는 위와 같이 server, client, HDesign이 모두 최상단에 존재했습니다.

/2024-haengdong
├── /server
└── /client
	├── lerna.json
	├── package.json
	└── packages
	    ├── haengdong-client
	    │   ├── package.json
	    │   └── src
	    │       └── index.tsx
	    └── haengdong-design
	        ├── package.json
	        └── src
	            └── index.tsx

lerna를 이용하여 haengdong-clienthaengdong-design을 한번에 관리하기 위해, client 내부 packages폴더로 두가지 폴더를 이동했습니다. 또한, 혼동 방지를 위하여 패키지 이름도 명확하게 변경하였습니다.

2. haengdong-design의존성 주입을 통해 client에서 변경 감지

2024-08-30.5.58.02.mov

작동 방식은 아래와 같습니다.

// client/packages/haengdong-client/package.json
...
"dependencies": {
    "haengdong-design": "*",
    ...
  },
...

haengdong-design의 의존성을 "*"로 표시하여 같은 모노레포 내부의 패키지를 가르키게 변경했습니다.

// client/package.json
...
"start": "npx lerna run build:watch --scope=haengdong-design --parallel & npx lerna run dev --scope=haengdong-client --parallel",
...

lerna의 scrpit에서 haengdong-design의 build:wiatch 스크립트를 실행시키고, haengdong-client의 dev 스크립트를 실행시킵니다.

// client/packages/haengdong-design/package.json
...
"build:watch": "tsc --watch & tsc-alias --watch",
...

build:watch 스크립트는 haengdong-design 스크립트에서는 빌드에 사용되는 tsc, tsc-alias--watch 접미사를 통해 변경을 감지하여 재 빌드되도록 합니다.

이 과정을 거치면 haengdong-client의 개발서버에서 배포된 haengdong-designnpm 을 가르키는 것이 아니라, 내부 package의 haengdong-design 을 가르키며, build:watch스크립트를 통해 디자인시스템의 변경이 생길때 마다 내부 package가 재 빌드되기 때문에, 바로 반영된 것을 확인할 수 있습니다.

3. lerna를 통한 패키지 통합 테스트 및 CI 수정

client/package.json의 스크립트는 아래와 같습니다.

// client/packag.json
...
"scripts": {
    "start": "npx lerna run build:watch --scope=haengdong-design --parallel & npx lerna run dev --scope=haengdong-client --parallel",
    "build": "npx lerna run build",
    "lint": "npx lerna run lint",
    "test": "npx lerna run test --scope=haengdong-client",
    "e2e": "npx lerna run cypress-run --scope=haengdong-client",
    "storybook": "npx lerna run storybook --scope=haengdong-design",
    "ci:versionup:patch": "npx lerna version patch --no-push --no-git-tag-version --yes && npm run commit-version",
    "ci:versionup:minor": "npx lerna version minor --no-push --no-git-tag-version --yes && npm run commit-version",
    "ci:versionup:major": "npx lerna version major --no-push --no-git-tag-version --yes && npm run commit-version",
    "commit-version": "git add . && git commit -m \"chore(release): v`node -p 'require(\"./lerna.json\").version'`\"",
    "release": "npx lerna publish from-package",
    "ci:release": "npx lerna publish from-package --yes",
    "graph": "npx nx graph"
  },
  ...

npx lerna run ~~ 명령어를 실행시키면 packages 내부의 모든 패키지에서 npm run ~~ 을 실행시킵니다. npx lerna run lint를 예로 들면 client에서도, design에서도 모두 npm run lint를 실행시킵니다. --scope=package-name을 사용하면 범위를 지정할 수 있습니다.

lerna를 이용하여 /client 경로에서도 내부의 haengdong-client, haengdong-design의 테스트 및 빌드를 한번에 진행할 수 있습니다.
따라서, workflow도 clientdesign 모두 일괄적으로 테스트 할 수 있도록 변경하였습니다.

name: Client Pull Request

on:
  pull_request:
    types: [opened, synchronize]
    branches: [main, fe-dev]
    paths:
      - 'client/**'

jobs:
  test:
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash
        working-directory: ./client

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.15.1'

      - name: Install dependencies
        run: npm install

      - name: Run build
        run: npm run build

      - name: Run lint
        run: npm run lint

      - name: Run test
        run: npm run test

      - name: Cypress test
        run: npm run e2e

4. lerna를 이용한 디자인시스템 릴리즈 PR 생성 CI

fe-dev branch에 버전을 신경쓰지 않고 계속 merge한 뒤 main 브랜치로 release할 때, 해당 workflow를 실행하면 release에 대한 pr이 생성됩니다.

이는 main에 merge되어야 적용되므로 별도로 main으로 향하는 PR을 만들겠습니다.

name: Design System Pull Request

on:
  workflow_dispatch:
    inputs:
      semver:
        description: 'New Version(semver)'
        required: true
        default: 'patch'
        type: choice
        options:
          - patch
          - minor
          - major

permissions:
  contents: write # 체크아웃과 commit을 위해 필요합니다.
  pull-requests: write  # PR 생성을 위해 필요합니다.

jobs:

  chromatic:
    name: Run Chromatic
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.15.1'

      - name: Cache dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: |
          cd client
          npm install

      - name: Run Chromatic
        uses: chromaui/action@latest
        id: publish_chromatic
        with:
          workingDir: client/packages/haengdong-design
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

      - name: Comment on PR
        uses: thollander/actions-comment-pull-request@v2
        with:
          message: '🚀 **storybook**: ${{ steps.publish_chromatic.outputs.storybookUrl }}'

  create-release-pr:
    name: Create Release PR
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./client
    steps:
      - name: checkout
        uses: actions/checkout@v3
      - name: Update Version
       
        # git의 정보를 불러오고, ci:versionup 스크립트를 통해 npx lerna version 을 실행합니다. 처음 workflow를 실행 할 때 선택한 SEMVER(patch, minor, major)가 적용됩니다.
        run: |
          git config --global user.email "${GIT_AUTHOR_EMAIL}"
          git config --global user.name "${GIT_AUTHOR_NAME}"
          npm run ci:versionup:${SEMVER} --yes
        env:
          SEMVER: ${{ github.event.inputs.semver }}
          GIT_AUTHOR_NAME: ${{ github.actor }}
          GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com

		# lerna.json을 통해서 현재 버전을 확인합니다.
      - name: Set PACKAGE_VERSION
        run: echo "PACKAGE_VERSION=$(cat lerna.json | jq -r .version)" >> $GITHUB_ENV
        
        # github release note를 작성합니다. 위에서 알아낸 package version을 이용합니다.
      - name: Set GitHub Release Note
        id: release_note
        uses: actions/github-script@v6
        with:
          script: |
            const result = await exec.getExecOutput(`gh api "/repos/{owner}/{repo}/releases/generate-notes" -f tag_name="v${process.env.PACKAGE_VERSION}" --jq .body`, [], {
              ignoreReturnCode: true,
            })
            core.setOutput('stdout', result.stdout)
        env:
          PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Create Pull Request
        id: cpr
        uses: peter-evans/create-pull-request@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "chore(release): v${{ env.PACKAGE_VERSION }}"
          committer: GitHub <[email protected]>
          author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
          assignees: ${{ github.actor }}
          signoff: false
          branch: release/${{ env.PACKAGE_VERSION }}
          branch-suffix: timestamp
          delete-branch: true
          title: 'v${{ env.PACKAGE_VERSION }}'
          body: |
            ${{ steps.release_note.outputs.stdout }}
            '🚀 **storybook**: ${{ steps.publish_chromatic.outputs.storybookUrl }}'
          labels: "Type: Release"
      - name: Check Pull Request
        run: |
          echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
          echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

image

위 workflow를 이용해 release PR을 만들었습니다. 해당 PR을 확인하고, main에 merge하게 된다면 아래와 같은 workflow가 작동합니다.

name: Design System Release

on:
  pull_request:
    branches:
      - main
    types: [ closed ]
  workflow_dispatch: # 강제로도 릴리즈 할 수 있도록 diapatch 존재

jobs:
  check:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./client
    permissions:
      contents: read
    if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
    outputs:
      EXISTS_TAG: ${{ steps.tag_check.outputs.EXISTS_TAG }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set PACKAGE_VERSION
        run: echo "PACKAGE_VERSION=$(cat lerna.json | jq -r .version)" >> $GITHUB_ENV

		# 현재 브랜치에 version Tag가 존재하는지 확인합니다.
      - name: Tag Check
        id: tag_check
        run: |
          GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}"
          http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s \
            -H "Authorization: token ${GITHUB_TOKEN}")
          if [ "$http_status_code" -ne "404" ] ; then
            echo "EXISTS_TAG=true" >> $GITHUB_OUTPUT
          else
            echo "EXISTS_TAG=false" >> $GITHUB_OUTPUT
          fi
        env:
          TAG_NAME: v${{ env.PACKAGE_VERSION }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  release:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./client
    needs: check
	# 위에서 확인한 태그가 없는 경우에만 실행합니다.
    if: always() && (needs.check.outputs.EXISTS_TAG == 'false')
    permissions:
      contents: write
      issues: write
      pull-requests: write
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20.15.1'
          registry-url: 'https://registry.npmjs.org'
      - name: Git Identity
        run: |
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@users.noreply.github.com'
          git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/$GITHUB_REPOSITORY
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

		# lerna.json을 통해서 package version을 확인합니다.
      - name: Set PACKAGE_VERSION
        run: echo "PACKAGE_VERSION=$(cat lerna.json | jq -r .version)" >> $GITHUB_ENV
      - name: Install
        run: npm install
      - name: Publish
		# ci:release 스크립트로 npx lerna publish를 실행시킵니다.
        run: npm run ci:release
        env:
        # 배포를 위한 npm token을 활용합니다.
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
		# 버전 변경을 담은 github tag를 설정합니다.
      - name: Create Git Tag
        uses: pkgdeps/git-tag-action@v2
        with:
          version: ${{ env.PACKAGE_VERSION }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          github_repo: ${{ github.repository }}
          git_commit_sha: ${{ github.sha }}
          git_tag_prefix: "v"
        # github release를 생성합니다.
      - name: Create Release
        id: create_release
        uses: softprops/action-gh-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: v${{ env.PACKAGE_VERSION }}
          # Copy Pull Request's tile and body to Release Note
          name: ${{ github.event.pull_request.title }}
          body: ${{ github.event.pull_request.body }}
          draft: false
          prerelease: false
          generate_release_notes: ${{ !github.event.pull_request.body }}
      - uses: actions/github-script@v6
        if: github.event_name != 'workflow_dispatch'
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🎉 Release https://github.com/${{ github.repository }}/releases/tag/v${{ env.PACKAGE_VERSION }}'
            })

@Todari Todari requested review from soi-ha, pakxe and jinhokim98 August 29, 2024 21:40
@Todari Todari self-assigned this Aug 29, 2024
@Todari Todari changed the title [FE] #531 테스트 [FE] client, designsystem 간 모노레포 의존성 설정 Aug 29, 2024
@Todari Todari added this to the lev4 milestone Aug 29, 2024
@Todari Todari linked an issue Aug 29, 2024 that may be closed by this pull request
2 tasks
Copy link
Contributor

@jinhokim98 jinhokim98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생많았어요~~ 덕분에 아주 편하게 행동디자인을 사용할 것 같아요
변경사항 470개 보고 좀 놀라긴했는데, 서버를 지워서 저렇게 된 것보고 안심했습니다ㅋㅋㅋㅋ

Comment on lines +1 to +6
{
"nodes": {},
"externalNodes": {},
"dependencies": {},
"version": "6.0"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr에 lerna를 사용한다고 되어있는데 nx가 있는 것은 실수로 남겨둔 것인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇽~ lerna 자체가 nx 기반으로 만들어진 거고, lerna graph를 위한 project-graph.json 파일이에요~!~!

"cypress-open": "cypress open",
"cypress-run": "cypress run",
"test": "jest"
"start": "npx lerna run build:watch --scope=haengdong-design --parallel & npx lerna run dev --scope=haengdong-client --parallel",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

진짜 갓갓이다.. 행동디자인을 바꿔도 바로 client에 적용할 수 있는 방법이 있다니

"commit-version": "git add . && git commit -m \"chore(release): v`node -p 'require(\"./lerna.json\").version'`\"",
"release": "npx lerna publish from-package",
"ci:release": "npx lerna publish from-package --yes",
"graph": "npx nx graph"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

graph 명령어는 어떤건가요?

Copy link
Contributor Author

@Todari Todari Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 현재 우리 레포가 2개밖에 없어서 크게 유용한 명령어는 아닌데, package들 끼리의 의존성을 graph로 한눈에 보기 좋게 보여주는 명령어입니다.
image

"@sentry/react": "^8.25.0",
"@tanstack/react-query": "^5.51.23",
"haengdong-design": "*",
"jest-canvas-mock": "^2.5.2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소하긴한데 이 의존성은 개발과 관련된 것 같아서 옮겨도 좋을 것 같아요.

@@ -11,6 +11,7 @@
"scripts": {
"start": "webpack serve ",
"build": "rm -rf dist && mkdir dist && tsc && cp -r ./src/assets ./dist && tsc-alias",
"build:watch": "tsc --watch & tsc-alias --watch",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

행동 디자인을 수정 후 build:watch 명렁어로 빌드를 해준 후에 사용하면 되는 것일까요?
근데 영상보면 수정하자마자 바로 반영이 되는 것 같은데;;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 있는 build:watch 명령어는 client 디렉토리에서 npm run start 명령어를 위해서 사용되는거에요
npm run start를 하면, haengdong-client 에서는 npm run dev, haengdong-design에서는 build:watch가 동시에 실행되는데, 여기서 변경사항이 생기면 다시 빌드되면서 npm run dev에 반영이 되는 방식입니다.

@Todari
Copy link
Contributor Author

Todari commented Sep 6, 2024

모노레포 적용 재 논의가 필요하여 close 합니다

@Todari Todari closed this Sep 6, 2024
@Todari Todari modified the milestones: lev4, v2.0.0 Sep 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

[FE] client, designsystem 간 모노레포 의존성 설정
2 participants