(5) Storybook 으로 구축하는 Design System – 배포 및 가져오기

시소·2024년 5월 31일
0

Design System

목록 보기
5/5
post-thumbnail

이 글은 Storybook의 공식 튜토리얼인 Design Systems for Developers를 학습하기 위해 작성한 시리즈의 일환으로, 오늘은 프로젝트를 패키징 하고 NPM에 업로드 하는 과정과, 해당 라이브러리를 다른 프로젝트에서 가져오는 방법에 대해 알아보았다.

들어가며

디자인 시스템의 주 목적인 '여러 애플리케이션에서 일관된 UI/UX를 제공하고 개발 효율성을 높이자'라는 미션을 성공적으로 이뤄내려면 어떻게 해야 할까?
지금까지의 시리즈에서는 디자인 시스템을 구축하는 전반적 과정에 대해 알아 보았으니, 이제는 우리의 디자인 시스템을 세상으로 내보내야 할 차례이다. 그래야 프로젝트에서 디자인 시스템을 라이브러리 형태로 설치해 사용하도록 만들 수 있다.
따라서 이번 편에서는 UI 컴포넌트를 효과적으로 패키징하고 배포하는 법에 대해서 알아보려 한다.


디자인 시스템 패키징 📦

어떻게 보면, 디자인 시스템도 우리가 개발하며 자주 사용하게 되는 여러 라이브러리(lodash, moment 등)들과 별반 다를 게 없다. 디자인 시스템을 이루는 다양한 요소들 역시, NPM과 같은 패키지 도구를 통해 쉽게 통합되거나 유지 관리될 수 있다.

따라서, 프론트엔드 개발 환경에서 가장 많이 사용되는 패키지 관리 도구 중 하나인 NPM 을 사용하여 디자인 시스템을 패키징 해볼 것이다.

프로젝트 구조

먼저, 완성된 프로젝트 구조는 이곳에서 확인할 수 있다.

처음에 프로젝트를 만들 때는 간편하게 구축하기 위해 Vite + React Setup을 참고해서 진행했는데,
라이브러리 개발 환경에는 Rollup 으로 빌드하는 게 적절할 것 같아, 빌드 옵션은 Rollup 기반으로 설정하였다.

지금까지의 프로젝트 구조에서, 일부 필요 없는 파일들을 제거하고 아래와 같은 디렉토리 구조를 갖도록 수정하였다.

📂 react-design-system
├── package-lock.json
├── package.json
├── src/
│   ├── Button/ # 버튼 컴포넌트
│   │   ├── Button.stories.tsx
│   │   ├── Button.tsx
│   │   └── index.ts
│   ├── Icon/ # 아이콘 컴포넌트
│   │   ├── Icon.stories.tsx
│   │   ├── Icon.tsx
│   │   └── index.ts
│   ├── shared/ # 공유 애셋
│   │   ├── assets/icons/
│   │   └── styles.css
│   ├── Colors.mdx # doc 관련 
│   ├── Icons.mdx # doc 관련 
│   ├── Intro.mdx # doc 관련
│   ├── index.ts
│   ├── types.d.ts
│   └── vite-env.d.ts
├── tailwind.config.js
├── postcss.config.js
├── rollup.config.mjs # 🆕
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

진입점 (src/index.ts)

라이브러리를 import 할 때 import { Button, Icon } from '라이브러리명' 과 같은 식으로 불러올 수 있도록 index 파일을 구성한다. 또한 css style이 적용될 수 있게 글로벌 CSS도 import 해준다.

import * as icons from "./shared/assets/icons";
import "./shared/styles.css";

export { icons };

export * from "./Button";
export * from "./Icon";

패키지 메타데이터 변경

라이브러리를 업로드 하기 전에 패키지 메타데이터를 적절하게 변경하기 위해, 터미널에서 다음 명령어를 입력한다. (프로젝트 루트 폴더에서)

# npm init --scope=@your-npm-username
> npm init --scope=@mnngfl

...

package name: @mnngfl/learn-react-storybook-ds
version: 0.1.0
description: Learn storybook design system
entry point: dist/cjs/index.js
git repository: (repository url)
author: (your-npm-username <your-email-address@email-provider.com>)
license: MIT

이후 변경된 package.json 파일을 확인해서 변경 사항이 제대로 반영되었는지 볼 수 있다.
또한 아래와 같이 프로젝트를 패키지로 배포하기 위한 몇 가지 핵심적인 요소에 대해서도 추가로 작성하자.

// package.json
{
  // ...
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "dependencies": {
    "autoprefixer": "^10.4.19",
    "class-variance-authority": "^0.7.0",
    "postcss": "^8.4.38",
    "tailwind-merge": "^2.3.0",
    "tailwindcss": "^3.4.4"
  },
  "devDependencies": {
    // ...
  },
  "peerDependencies": {
    "react": "^17.0.0 || ^18.0.0",
    "react-dom": "^17.0.0 || ^18.0.0"
  }
}

Rollup 빌드 설정

빌드에 필요한 의존성들을 설치한다. 또한 react와 react-dom은 peerDependency로 만들어 줬다. (라이브러리 소비자가 제공할 것으로 예상되므로 번들에 포함하지 않도록 하기 위해)

> npm i -D @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve  \
           @rollup/plugin-typescript @rollup/plugin-url @svgr/rollup \
           @babel/preset-env @babel/preset-react @babel/preset-typescript \
           rollup rollup-plugin-dts rollup-plugin-peer-deps-external \
           
> npm uninstall react react-dom
> npm i --save-peer react react-dom
// rollup.config.mjs
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from "@rollup/plugin-commonjs";
import peerDepsExternal from "rollup-plugin-peer-deps-external
import typescript from "@rollup/plugin-typescript";
import svgr from "@svgr/rollup";
import url from "@rollup/plugin-url";";
import { babel } from "@rollup/plugin-babel";
import { dts } from "rollup-plugin-dts";
import postcss from "rollup-plugin-postcss";

import { createRequire } from "node:module";
const requireFile = createRequire(import.meta.url);
const packageJson = requireFile("./package.json");

export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
      },
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      nodeResolve(), /* 서드파티 모듈 사용*/ 
      peerDepsExternal(), /* peerDepencency 번들에서 제외 */ 
      commonjs(), /* CommonJS 모듈을 ES6로 변환 */ 
      
      /* TypeScript 설정 */
      typescript({
        tsconfig: "./tsconfig.json",
      }),
      
      /* Babel 트랜스파일러 설정 */
      babel({
        babelHelpers: "bundled",
        sourceType: "unambiguous",
        presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
        extensions: [".js", ".jsx", ".ts", ".tsx"],
        exclude: "node_modules/**",
      }),
      
      /* SVG icon 관련 설정 */
      url({
        include: "**/*.svg",
        limit: 8192,
        emitFiles: true,
        fileName: "[name][hash][extname]",
      }),
      svgr(),
      
      /* PostCSS 설정 (CSS를 JS 번들에 포함시킴) */
      postcss({
        extensions: [".css"],
        inject: true,
        extract: false,
      }),
    ],
    external: ['react', 'react-dom', 'tailwindcss', 'autoprefixer', 'postcss'],
  },
  {
    input: "dist/esm/types/index.d.ts",
    output: [{ file: packageJson.types, format: "esm" }],
    plugins: [dts()],
    external: [/\.css$/, 'class-variance-authority/types'],
  },
];
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "skipLibCheck": true,
    "allowJs": false,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",

    /* Emit */
    "declaration": true,
    "emitDeclarationOnly": true,
    "sourceMap": true,
    "outDir": "dist",
    "declarationDir": "types",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true,
    "allowUnreachableCode": false
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

이제 내보내기를 위한 기본적인 설정이 마련되었다.

Auto를 통한 릴리즈 관리

Auto는 릴리즈 관리를 자동화하기 위해 설계된 오픈소스 도구이다. 주요 기능으로는 다음이 있다.

  • 🚌 릴리즈 관리: NPM에 릴리즈 자동 배포
  • 🔄 변경 사항 기록: 변경 사항을 설명하는 CHANGELOG 자동 업데이트
  • 🤖 버전 관리: 적절한 버전 번호 자동 설정
  • 🏷️ Git 태그 생성: 버전 번호를 커밋과 연결하는 Git 태그 자동 생성

해당 도구를 활용하면 릴리즈 프로세스를 단순화 하고 효율적으로 만들 수 있다. 아래는 그 사용법이다.

(사전 준비) GitHub 및 NPM 액세스 토큰 가져오기

repoworkflow 스코프를 갖는 GitHub 토큰, Publish 권한을 갖는 NPM 토큰을 발급해 준다.

그런 다음 프로젝트 .env 파일에 아래 토큰 정보를 저장한다.

// .env
...
GH_TOKEN=<value you just got from GitHub>
NPM_TOKEN=<value you just got from npm>

(🛑 토큰 정보가 GitHub에 업로드되지 않도록, .gitignore 에 .env 파일을 포함하였는지 다시 한 번 확인하자.)

(사전 준비) GitHub에 레이블 만들기

> npx auto create-labels

깃허브 레파지토리 > Issue 탭에서 Labels를 클릭하면 레이블이 자동 생성된 걸 볼 수 있다. 향후 PR 요청을 만들 때, 아래 레이블 중 하나를 태그로 지정해 주면 적절한 Change log 메시지가 기존 메시지에 자동으로 덧붙여 진다. (상세 내용 확인 <- "Click here to see the default label configuration" 클릭)

Auto를 활용한 첫 번째 수동 릴리즈

스크립트와 Auto 도구로 릴리즈와 관련된 전 과정을 자동화 할 수 있지만, 우선 Auto에 어떤 기능이 있는지 이해하기 위해 수동으로 명령어를 실행하며 릴리즈 하는 방법에 대해 알아보았다.

① Change log 만들기

# 지금까지 생성된 모든 커밋 내역이 담긴 `CHANGELOG.md` 파일 프로젝트 루트에 생성됨
> npx auto changelog

# (선택 사항) 사용자가 보기에 모든 커밋이 너무 장황할 수 있으므로, 
# changelog 메시지를 간략하게 업데이트 하는 방법
> git reset HEAD^ # 방금 전 커밋 취소, 이후 changelog 파일을 원하는 메시지로 수정

> git add CHANGELOG.md
> git commit -m "Changelog for v0.1.0 [skip ci]" # 변경 사항 푸시

② 프로젝트 최종 Publish, GitHub 릴리즈 생성

> npm --allow-same-version version 0.1.0 -m "Bump version to: %s [skip ci]"
> git push --follow-tags origin main

> npm publish --access=public # npm에 패키지 배포
> npx auto release --use-version v0.1.0 # GitHub에 릴리즈 생성

이제 이러한 릴리즈 과정을 자동화 하는 구성 방법에 대해서도 알아보자.

GitHub Actions와 Auto를 활용한 자동 릴리즈 구성

① 프로젝트 Secrets에 NPM 토큰 추가

  • 프로젝트 Repository > Settings > Secrets and variables > Actions
  • "New repository secret" 버튼 클릭
  • Name: NPM_TOKEN, Secret: 앞부분 NPM에서 얻은 자신의 토큰
  • 이후 secrets.NPM_TOKEN 으로 액세스 가능

② 프로젝트에 Auto 의존성 설치 및 npm 스크립트 작성

  • npm i -D auto
  • package.json: scripts.release: "rollup -c && auto shipit"

③ 프로젝트에 GitHub workflow 추가 후 커밋

# .github/workflows/push.yml
name: Release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
    steps:
      - uses: actions/checkout@v4
      - name: Prepare repository
        run: git fetch --unshallow --tags
      - name: Use Node.js 18.x
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Install dependencies
        uses: bahmutov/npm-install@v1
      - name: Create Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          yarn release

④ GitHub Actions Permission 설정

  • 설정하지 않고 진행했더니 NPM에 패키지 배포는 진행되는데, GitHub에는 릴리즈가 안되는 에러 발생
  • 해결 방법: 프로젝트 Repository > Settings > Actions > General > Workflow permissions 에서 "Read and write permission" 선택 후 저장
  • 참고 링크: https://stackoverflow.com/a/75175628
  • 에러 메시지 전문: 👇
Error: Running command 'git' with args [push, --follow-tags, --set-upstream, https://github.com/mnngfl/react-design-system, main] failed

remote: Permission to mnngfl/react-design-system.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/mnngfl/react-design-system/': The requested URL returned error: 403

⑤ 자동 릴리즈 동작 테스트

  • feature 브랜치에서 커밋 후, feature -> main Pull Request 생성
  • 적절한 PR labels 선택하기 (레이블에 따라 change log 메시지 상이)
  • Merge 완료되면 Actions 탭에서 "Release" workflow 확인
  • 성공적으로 수행되면 NPM Package, GitHub Release 버전 업데이트
  • v0.1.1 에서 CI 권한 에러 수정하여 v0.1.2로 최신 버전 업데이트된 모습: 👇


디자인 시스템 가져오기 📥

이제 디자인 시스템이 NPM을 통해 배포 되었으니, 다른 프로젝트에서 실제로 설치하고 사용해보도록 하자!

신규 프로젝트를 직접 만들진 않았고, 온라인에서 Vite를 사용할 수 있는 StackBlitz에서 테스트를 진행하였다.

프로젝트 코드 데모

https://stackblitz.com/edit/vitejs-vite-nej76g?file=src%2FApp.tsx

사용 방법

① 의존성 설치

참고로 이 디자인 시스템은 React를 이미 사용하고 있는 프로젝트라고 가정하고 해당 라이브러리가 peerDependency로 지정됐기 때문에, 신규 프로젝트 역시 React 개발환경이 구성되어 있어야 한다.

> npm i @mnngfl/learn-react-storybook-ds

② UI 컴포넌트 import

import { Button, Icon } from '@mnngfl/learn-react-storybook-ds';

...
<Button variant="outlined" label="Click me!" icon="mail" />
<Button size="lg" label="Large" />
<Icon name="mail" <variant="primary" />

이렇게 디자인 시스템으로 만든 컴포넌트를 예제 프로젝트에서 불러올 수 있게 되었다!


마치며

이로써 Storybook을 주축으로 하는 디자인 시스템 만들기에 대한 Workflow를 한 사이클(?) 돌려 보았다. 컴포넌트 개발, 테스트, 문서화 그리고 배포라는 모든 단계를 거쳐 디자인 시스템을 효과적으로 만들고 관리하기 위해 권장되는 방식을 체험해 보았다.

사실 직접 배포까지 진행해보면서 느낀 점은, 디자인 시스템을 구축한다는 것이 시간과 노력이 많이 드는 작업임을 뼈저리게 느끼게 되었다.. 아무래도 TypeScript와 Rollup을 통한 구성이 익숙하지는 않다 보니 삽질에 오랜 시간이 소요된 것 같다.
또한 웹 애플리케이션을 만드는 작업과 라이브러리를 만드는 작업은 참 다른 것이구나. 이런 저런 고려사항이 많다고 느꼈다.

따라서 지금 버전이 최종 버전은 아니고, 더 여러 종류의 컴포넌트도 추가해 보고, 구성 방법에 대한 공부도 더 해서 한번 꾸준히 발전시켜 보고 싶다. 그러다 보면 좀 더 완성도 있는 디자인 시스템을 만들 수 있게 되지 않을까..👊

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

0개의 댓글

관련 채용 정보