🐰

デザインシステムをマルチフレームワーク(React/Vue.js)に対応させてみた

2024/05/27に公開

はじめに

こんにちにんにん、 art です。

僕が所属するレバテック開発部では、最近デザインシステム『VoLT』が誕生しました。

レバテックでは複数のプロジェクトが運用されており、フロントエンドの技術スタックは

の2つが採用されています。
ですので、両方の技術スタックに対応したデザインシステムを作る必要があるわけですね。
あら大変。_(:3 」∠ )_

というわけでこの記事では、『VoLT』のUIコンポーネントライブラリを制作するにあたり、肝となったデザインシステムのマルチフレームワーク対応についてお話しします。

🔗そもそも『VoLT』とはなんぞや?構築の背景は?と気になった方は、こちらをご覧ください!

https://zenn.dev/levtech/articles/efedca53668140
https://note.com/leverages_design/n/n5e2a397602ba

想定読者

  • デザインシステムに興味のある方
  • これからデザインシステム関連の業務に携わる方
  • すでにデザインシステム関連の業務に携わっている方
  • (広く)フロントエンド開発者

マルチフレームワーク対応にあたり発生した問題

基盤構築を進めるにあたり、マルチフレームワーク対応特有の問題が想定されました。
主な問題は下記2点です。

  • フレームワーク間で同等の機能提供を担保できるか
  • Storybook を一つのURLから一覧できるか

フレームワーク間で同等の機能を提供できるの?

例えば Checkbox のコンポーネント1つを作ろうと思ったら、React と Vue.js それぞれのコンポーネントを作る必要があります。

みな限られたリソースの中で開発していますので、React と Vue.js のコンポーネントで実装者が異なるということは往々にして起こり得ます。

そんな中で、フレームワークに捉われない部分はできる限り共通化しようと思い至りました。
コンポーネントを作成するために必要な要素としてスタイル(CSS)やプロパティ(Props)がありますが、これらはフレームワークに依存しませんし、
開発体験を損なわないためには linter などの開発サポートツールもできるだけ共通しておくのが望ましいと考えました。

Storybook を一つのURLから一覧できるの?

デザインシステムのプロジェクトが進んでいく中で、外部公開するかどうかの話題が上がりました。
徐々に公開していきたいという意思決定がされ、そうなると React と Vue.js それぞれ別々のURLで公開するのは微妙じゃないかという懸念点が浮かび上がってきました。(まあそうなりますよねw

ですので、Storybook の仕組み上それが可能かという観点も含めて検討材料に入れる運びとなりました。

どんな基盤を作ったか

では、上述した問題を踏まえて、実際にどのようなシステム基盤を作ったのかを紹介します。

ディレクトリ構成

前提のお話ですが、『VoLT』では

  • デザイントークンライブラリ
  • UIライブラリ(React/Next.js用)
  • UIライブラリ(Vue.js/Nuxt.js用)

の3つのライブラリを用いてデザインシステムをシステム開発に反映できるようにしています。

まずは全体のディレクトリ構成を共有します。

.
├── tokens # デザイントークンライブラリ
└── ui # UIコンポーネントライブラリ群

🔗デザイントークンライブラリについて詳しく知りたい方は、こちらの記事をご覧ください!
https://zenn.dev/levtech/articles/c6d3bfe6a8911d


次に、UIコンポーネントライブラリにフォーカスしたディレクトリ構成を共有します。

./ui
├── apps # アプリケーション群
│   ├── react # React 向けUIコンポーネントライブラリ
│   ├── storybook # フレームワーク別の Storybook を一覧するための Vite アプリケーション
│   └── vue # Vue.js 向けUIコンポーネントライブラリ
├── packages # パッケージ群
│   ├── xxx-config # config 系
│   └── ui # フレームワーク共通のUI構成パーツ(types, styles)
├── turbo.json
└── package.json

このように、ui ディレクトリ下は Turborepo を用いた Monorepo 構成を採用しました。

Monorepo によりフレームワーク間のコンポーネント構成要素を共通化

UIコンポーネントライブラリを管理するディレクトリを Monorepo 構成にすることによって、フレームワーク間でスタイルとプロパティの型を共有できるという恩恵を受けることができます。
(コンポーネントの見た目と制御するプロパティはあらかじめデザインの時点で確定しているため、これらの情報はフレームワークによって差分は出ないという想定に基づきます。)

これにより多重定義を避け、フレームワーク間で同じスタイルと型を参照することで同等の機能提供を担保しています(これだけで全てをカバーできるわけではありませんが><)。
余計なファイルを省くという意味では、保守性の向上/開発生産性の向上も見込めます。

↓実装例をご覧ください。

  1. 共通スタイルファイルを作成
ex. packages/ui/src/Grid/Grid.scss
@use "@lv-levtech/volt-tokens/dist/index.scss" as *;

.volt-Grid {
  display: grid;

  &[data-gap="4"] {
    gap: $space-4;
  }

  // ...
}
  1. 共通型ファイルを作成
ex. packages/ui/src/Grid/Grid.type.ts
type Gap = 4 | 8 | 12 | 16
export interface GridProps {
  /** グリッドの間隔 */
  gap?: Gap;

  // ...
}
  1. React 向けUIコンポーネントライブラリで 1, 2 を使用
ex. apps/react/src/Grid/Grid.tsx
import { GridProps } from '@volt-ui/ui';
import '@volt-ui/ui/dist/styles/Grid.scss';

interface Props
  extends GridProps,
    Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {}

export const Grid = forwardRef<HTMLDivElement, Props>(
  (
    {
      children,
      gap,
    },
    ref
  ) => {
    return (
      <div
        ref={ref}
        className={'volt-Grid'}
        data-gap={gap}
      // ...
    )
  }
}
  1. Vue.js 向けUIコンポーネントライブラリで 1, 2 を使用
ex. apps/vue/src/Grid/Grid.vue
<template>
  <div
    class="volt-Grid"
    :data-gap="gap"
  // ...
</template>

<script setup lang="ts">
import { GridProps } from '@volt-ui/ui';

const props = withDefaults(defineProps<GridProps>(), {
  gap: undefined,
});

// ...
</script>

<style scoped lang="scss">
@use '@volt-ui/ui/dist/styles/Grid.scss';
</style>

このように、 @volt-ui/ui という共通パッケージを媒介として React/Vue.js で共通の機能を使うことができます。
Monorepo に精通した方は馴染みのある構成かと思います。

フレームワーク別の Storybook を一覧できる Vite アプリケーションを作る

Storybook, at its core, is powered by builders such as Webpack and Vite. These builders spin up a development environment, compile your code—Javascript, CSS, and MDX—into an executable bundle and update the browser in real-time.[1]

訳:
Storybook は、本質的には Webpack や Vite などのビルダーによって動作します。これらのビルダーは開発環境を起動し、コード (Javascript、CSS、MDX) を実行可能なバンドルにコンパイルし、ブラウザをリアルタイムで更新します。

とあるように、Storybook は使用技術によって異なるビルダーを用いて動かします。

React/Vue.js も例に漏れず、React だと @storybook/react-vite、Vue.js だと @storybook/vue3-vite といったライブラリを使うのが一般的かと思います[2]

今回は開発時ではなく公開時に static file を何らかの形で一つにまとめられれば良かったので、 storybook build コマンドで生成した storybook-static というフォルダの構成に着目しました。

storybook-static のフォルダ構成

ルートディレクトリの index.html にアクセスすることで Storybook を閲覧できるようになっています。
React と Vue.js 2つのアプリがあるので、 storybook-static もそれぞれ作成されます。

HTMLファイルにアクセスすればいいということは iframe の仕組みを使えば2つの Storybook を切り替えることは容易なので、この方法を採用しました。

↓作ってみた画面がこちら

表示領域が小さいとちょっと見づらいですが、右下のアイコンをクリックすることで React/Vue.js の Storybook が切り替わるようになっています。

※まだ絶賛開発段階なので、コンポーネントはこれからたくさん作られます✌︎('ω'✌︎ )

おわりに

いかがでしたでしょうか。

世に公開されているデザインシステムは多くありますが、マルチフレームワークに対応したデザインシステムは少数かと思います。

ということは同じような状況に出会している方は少ないのかなと思いつつも、
この記事が誰かの一助になれば幸いです!

脚注
  1. Builders • Storybook docs から引用。 ↩︎

  2. Vite を採用しているプロジェクトを想定しています。webpack 用の builder などももちろんあります。 ↩︎

レバテック開発部

Discussion