🪐

デザインシステムに準拠したコンポーネント駆動UI開発への取り組み

2022/11/14に公開

こんにちは、THECOO株式会社のpoteboyと申します。普段は現職1人目のWEBフロントエンド開発者としてFaniconというファンコミュニティサービスの開発を行っております。

弊社では私が入社する以前までフロントエンド専任の開発者がおらず、サーバーサイドの開発者やAndroidの開発者がフロントエンドの開発業務を兼務していました。

専任がいなかったのは、Faniconが元々ネイティブベースのアプリで、WEBアプリの開発優先度を下げていたためです。

そのため、フロントエンドの開発環境に関しても万全とは言えず、2022年6月に私が入社し、9月には2人目のフロントエンド開発者となるTomoya氏が入社したタイミングで、2人で協力してFaniconフロントエンドの大改修を行なうこととなりました。

フロントエンド改修業務にはTypeScriptの導入、既存コンポーネントのVue3化、単体テストやE2Eテストの導入など多岐に渡りますが、本記事ではその中でも私が弊デザイナーのムラキ氏の協力の元で特に力を入れて取り組んでいる 『デザインシステムに準拠したコンポーネント駆動UI開発への取り組み』 についてご紹介させて頂きたいと思います。

想定読者

  • 小規模チームで開発するフロントエンド開発者
  • フロントエンド開発環境が整っていない既存プロダクトに途中から入った方
  • デザインシステムやコンポーネント駆動について知りたい方

我々の定義するコンポーネント駆動UI開発

実際の取り組みを紹介する前に、まずは各用語の定義を交えながらコンポーネント駆動開発の利点について紹介したいと思います。

コンポーネントとは何か

現代フロントエンドにおいては当たり前の概念となった「コンポーネント」について、本記事においては改めて 「見た目・振る舞いの機能をカプセル化したGUIモジュール群」 と定義します。

コンポーネントはまるでレゴブロックの組み合わせのようなもので、レゴブロック単体では殆ど意味をなさずとも、組み合わせることで城や宇宙船など巨大な建築物を作り上げることができます。

例えば以下の画像はFaniconで実際に使用されている画面の一部ですが、これらもText,Stack,Alert,Inputという弊社デザインシステムで定義されたコンポーネントのみで構成されています。

このようにコンポーネントを組み合わせてページを構成する分割統治法的な考え方をコンポーネント指向と呼びます。ここで注意が必要なのが、UIを構成する最少パーツ群だけがコンポーネントではありません。複数コンポーネントの集合体もコンポーネントであり、故にページ1画面もコンポーネントであると言えます。

コンポーネント駆動開発(CDD)とその利点

コンポーネント駆動開発(CDD)とは、UIを作り上げる際に小さなコンポーネントから作り始め、それらを組み合わせて最終的にページを作り上げるボトムアップな開発手法のことを指します。

この手法には以下に述べるいくつか利点があります。

品質の保証

CDDでは、最小単位のコンポーネント(以下基底コンポーネントと呼ぶ)を組み合わせて上位の複雑なコンポーネントを作り、さらにそれらコンポーネントを組み合わせることでページを作ります。この時基底コンポーネントを含む下位コンポーネントの品質が担保されていれば、それらを組み合わせた上位のコンポーネントの品質も高まることが帰納的に言えるでしょう。

上記より、基底コンポーネントとページを構成する各コンポーネントの単体テストカバレッジが保証できていれば、各ページに対するテストはビジネスロジック部分のみに焦点を当てることができます。また、ロジックも全てカスタムhookに切り出してテストすることでページ単位の品質向上に繋がります。

開発速度の向上と認知負荷の低下

ページ単位でHTMLやCSSをイチから書いていく従来のUI開発手法(所謂Page-based Development)では、同一な見た目をを持つ要素が至る所で再定義されるのでDRY原則に反し、認知負荷が高くなります。

CDDでは、各基底コンポーネントの役割・使い方、見た目としての振る舞いがチーム内で適切に共有されている場合に、UIの開発は基底コンポーネントを使い回すだけで誰でも簡単に遂行することができます

Faniconのフロントエンドが抱えていた課題

私が入社した2022年6月段階において、Faniconのフロントエンドはまさに前述したPage-basedな手法で開発されていました。

Page-basedな開発手法のすべてがCDDに劣るというわけではありませんが、Page-basedな手法が有効的なのは以下の2つの場面に限定されると私は考えています。

  • ReactやVueなどのコンポーネント指向を前提としたフレームワーク[1]で構築されていない場面
  • WEB・LP制作など(WEBアプリケーションと比較して)要素が使い回されにくい場面

FaniconはVue製のWEBアプリケーションである上に、私が入った段階で既にデザイントークンやコンポーネントパターンがFigma上で定義されていて実装上要素の再利用が可能であった[2]ので、Page-basedよりもCDDの方が相性が良さそうな印象を受けました。問題は、フロントエンドの開発現場でCDDやデザインシステムの存在が全く浸透していない事でした。

なぜCDDやデザインシステムが浸透していなかったのか

こうなってしまった背景として、冒頭でも記述した通り、Faniconは元々ネイティブアプリに注力したプロダクトであったことが挙げられます。

冒頭で紹介したデザイナーのムラキ氏が2021年10月に入社すると同時にスタイルガイドとデザイントークンの定義を進めていたので、私が入社した段階でネイティブアプリ側では一部デザイントークンの実装への反映が既に完了していました。

しかし、先述の通りFaniconには元々フロントエンド開発者がいなかったので、せっかく整備されつつあったデザインシステムの恩恵をWEB側は受けていなかったのです。

これらの状況を踏まえ、Faniconのフロントエンドの開発手法をPage-basedからコンポーネント駆動へと方向転換させることを目指して、実装側でデザインシステム基盤の構築を進め始めました。

(余談ですが、Figma上でデザイントークンやコンポーネントを定義する方法は以下の記事にて詳しく解説しているので、興味がございましたら併せてご確認ください👇)

https://zenn.dev/poteboy/articles/e236b87250b26c

我々の定義するデザインシステム

ここで一度、本記事(及びFanicon)においてデザインシステムが何を指すのかについて明確にしておこうと思います。

デザインシステムという言葉の定義は各組織に異なりますが、我々は以下の4つのフルセットを総称してデザインシステムと呼んでいます。

  • ブランド・アイデンティティ
    • 製品の目指す方向性・VISION・雰囲気・ロゴやカラーなどブランドを構成する要素を明示化したもの
  • スタイルガイド
    • ブランドアイデンティティをプロダクトに落とし込むために使用するインターフェースガイドライン
  • パターンライブラリ
    • スタイルガイドを実装・運用するためのコード・システム群
  • 上記運用のための諸々の仕組み
    • 更新の自動化、テスト、ドキュメンテーションツールなど諸々の運用のための仕組み

各用語の定義が何を指すのかについては組織によって変動するので、大切なのは何をどう呼ぶかではなく、チーム全体で共通の認識を持っていることだと以下の記事では述べられています。

https://product-unicorn.com/design-systems-style-guides-all-those-libraries-what-the-hell-is-the-difference-4c2741193fdc

Faniconにおけるデザインシステム基盤構築の技術選定

外資系企業や一部国内企業など豊富な資金源のある開発組織においては、プロダクト開発チームとは別にデザインシステム専門のチームを抱えていることも珍しくありません。

一方で弊社のように小規模な開発チームにおいては、人員リソースが限られているのでリソース削減のためにOSSとして提供されるUIライブラリを導入するケースが多い印象です。MUIChakra UIなどのライブラリでは、デフォルトのテーマをOverrideし自社のデザインシステム仕様にカスタマイズできる機能を提供しています。

import { red } from '@mui/material/colors';
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      main: red[500],
    },
  },
});

しかし、Faniconにおいては以下の理由でUIライブラリを使用せずフルスクラッチでデザインシステム基盤を実装する運びとなりました。

  1. 2022年6月当時においてVue3に対応するUIライブラリが殆ど存在しなかったこと
  2. フロントエンドを書く他の開発者[3]への学習コストが高かったこと
  3. Faniconが直近でVueの破壊的バージョンアップ(2→3)を経験したこともあり、FW側の都合で今後UIライブラリの互換性が失われる可能性を否定できなかったこと

代わりに、作成したコンポーネントのカタログ兼ドキュメンテーションとしてStorybookを導入し、コンポーネントのVisual Regression TestツールにはChromaticを導入することの合意が取れました。これらのツールの活用法については後述します。

具体的な実装方針

ここからは実際にFaniconに実装した各種デザイントークン、共通コンポーネント、UI変更の自動検知システムの具体例をご紹介したいと思います。

デザイントークン

デザイントークンは、W3CというWeb技術の標準化を行っている団体によって以下のように定義されています。

Design tokens are indivisible pieces of a design system such as colors, spacing, typography scale.(デザイントークンとは、色、スペーシング、タイポグラフィのスケールなど、デザインシステムを構成する不可分なピースのことです。)
出典: https://github.com/design-tokens/community-group#design-tokens

こちらに倣い、Faniconでも色、スペーシング、タイポグラフィについて以下のように実装に落とし込んでいます。

まず色について、Faniconではプリミティブ色セマンティック色に大別して定義しています。

プリミティブ色はRed,Yellow,Greenなどの表色系によって大別される色を彩度毎に段階分けしたパレットから構成されており、基本的にはTailwindのカラーパレットを踏襲し、一部Faniconのブランドカラーに沿ってOverrideする形にしています。

export const themeColors = {
  Green: {
       500: '#34C759',
       600: '#16A34A'
  }
} as const

セマンティック色は色の意味や用途毎に定義されており、必ずプリミティブ色を参照する形としています。この意図として、ブランドのデザインを根本的に変えるとなったタイミングでセマンティック色の定義元を変更するだけで済むのと、プリミティブな色を直接使うときはその色がデザイン原則で定められたスタイルガイド以外の用途であることが明示されるからです。

Primary: themeColors.Orange[600],
Secondary: themeColors.Blue[600],

また、色はReadonlyな定数で定義し、以下の型定義に沿うようにしています。

type RGB = `rgb(${number}, ${number}, ${number})`
type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`
type HEX = `#${string}`
export type Color = RGB | RGBA | HEX

タイポグラフィ

タイポグラフィは、Textというコンポーネントを設け、そこのvariantにデザインシステムで定義されたトークンを渡す形で実装しています。HTML意味論の観点でタグ自体を変えたいときは、asというpropsから変更できるように実装しています。

<Text variant="paragraph-small" as="li">現在、国外配送は未対応です。</Text>
<Text variant="paragraph-small" as="li">配送事業者のセンター受け取りサービスなどはご利用いただけません。</Text>

スペーシング

Faniconは4 Porint Grid Systemを採用し、余白は基本的に4の倍数としています。

そこで、次のような関数を作り、Length型で明示的に長さを指定せずnumber型を指定した際は自動的に4倍pxを返すようにしています。

type AbsoluteLengthUnit = 'cm' | 'mm' | 'Q' | 'in' | 'pc' | 'pt' | 'px'
type RelativeLengthUnit = 'em' | 'ex' | 'ch' | 'rem' | 'lh' | 'vw' | 'vh' | 'vmin' | 'vmax'
type LengthUnit = AbsoluteLengthUnit | RelativeLengthUnit | '%'
export type Length =
  | `${number}${LengthUnit}`
  | `${number}${LengthUnit} ${number}${LengthUnit}`
  | `${number}${LengthUnit} ${number}${LengthUnit} ${number}${LengthUnit}`
  | `${number}${LengthUnit} ${number}${LengthUnit} ${number}${LengthUnit} ${number}${LengthUnit}`
  | `calc(${string})`
  | 'fit-content'
  | 'max-content'
  | 'min-content'
  | 'initial'
export const numberOrLengthToLength = (input: number | Length): Length => {
  if (typeof input !== 'number') {
    return input
  }
  return `${input * 4}px`
}

また、弊デザインではFigma上でAuto Layoutが多用されることから、実装側でもStack,HStack,VStackというコンポーネントを用意し、Figmaでデザイナーが作成した通りにUIに落とし込むことができるようにしています。

<VStack as="ul" space="2" v-for="tab in props.tabs" :key="tab.url">
  <Text variant="label-medium" as="li">{{ tab.label }}</Text>
</VStack>

AutoLayoutはCSSにおけるFlexboxに対応しているので、上記実装はCSSで書くと以下のようになります。先ほどspacepropに渡された2という整数は、numberOrLengthToLength関数を通して、4 Point Grid Systemに合うように8pxに変換されているのが分かります。

ul {
 display: flex;
 flex-direction: column;
 gap: 8px;
 li {
   font-size: 16px;
   font-weight: bold;
   line-height: 24px;
 }
}

共通コンポーネント集

リファクタリングや設計の本では、「3度目の法則」という言葉がよく登場します。これは同じ処理が3度出てきたタイミングで初めて共通化するという指標で、コンポーネントの共通化の話でも度々引用されています。

しかし、弊フロントエンドではデザインシステムとして定義するコンポーネントは、デザイントークンを表現するコンポーネントか、デザイナーがFigma上で共通コンポーネントとして切り出したもののみとしており、フロントエンド開発者の独断でデザインシステムコンポーネントとして共通化しないようにしています(フロントエンド開発者側からデザイナーに共通化を提言するケースもあります)。

この理由として、フロントエンド開発者がFigmaデータを見てUIを実装する際、特定パーツがマスターコンポーネントのインスタンスであれば、デザインシステムとして既に定義されている(orされるべきである)ことが一目で分かるので、デザイナーの想定しないコンポーネントをデザインシステムとして混在させたくないという背景からです。

コンポーネントのインスタンスは、左側のパネル上で紫色のひし形の枠線で表示されているので、該当コンポーネントを右クリックすれば定義元に飛ぶことができます。

先ほど紹介したデザイントークンを表現するTextStackといったコンポーネントに加えて、デザイナーがデザインシステムとして定義したコンポーネントを総して実装側でデザインシステムコンポーネントとして定義し、UIを構築する際は基本的にこれらのコンポーネントを組み合わせる形で実装しています。

また、作成したデザインシステムコンポーネントはCIでStorybookに自動デプロイし、随時デザイナーが挙動を確認できるようにしています。

下記の画像は弊デザインシステムで定義されているTextInputというコンポーネントですが、Storybook側でpropsを指定することができるので、見た目に加えて振る舞いの確認も同時で行えるようになっています。

Figma上のデザインデータはあくまで中間成果物であるのに対し、Storybookでデプロイされたコンポーネントは全て本番環境で使用されているものと同一の振る舞いをするので、より正確な成果物としてもStorybookを活用しています。

StorybookとChromaticを活用したUI変更の自動検知

Storybookが公式で提供しているIntroduction to design systemsという記事の中で、デザインシステムは信頼できる唯一の情報源(Single Source of Truth)であると同時に、単一障害点(Single Point Of Failure)であると述べられています。デザインシステムコンポーネントが1つ壊れれば、それを使用している全てのページ・コンポーネントが同時に壊れるからです。

弊フロントエンドチームにおいて、デザインシステムコンポーネントの作成業務は日々の新規機能開発・保守運用業務に付随する形で行われています。それ故、デザインシステムコンポーネントを作成する際に始めから振る舞いを全パターン網羅する形で実装するのは工数の観点で現実的ではないので、YAGNIに則りタスクで必要になったタイミングで修正しています。

しかし、デザインシステムコンポーネントは単一障害点でもあるので、安易な変更は本番環境にデグレを発生させる恐れがあります。そのため、弊フロントエンドチームではコンポーネントを修正した際のデグレ防止目的でChromaticというツールを導入して、Visual Regression Test(以下VRT)を実施しています。

Chromaticは、Story定義されたコンポーネントに見た目上の変更があった際に以下の画像のように緑の差分を表示してくれるので、QAを待たずにバグを未然に防ぐことができます。

また、ChromaticによるVRTはデザインシステムコンポーネントのみにとどまらず、ページ全体に対しても実施しています。

冒頭でコンポーネント駆動(+単体テスト)の利点としてページの振る舞いが担保されるという全称命題を述べましたが、見た目に関しては必ずしも真ではありません。例えば、あるページがコンポーネントA、コンポーネントB、コンポーネントCから構成されていた時、コンポーネントBがz-indexabsoluteなどStacking contextに影響を及ぼすスタイルを含んでいた場合、他のコンポーネントA、Cに意図せぬ影響を与えてしまう可能性があるからです。

そこで、見た目の正しさの担保としてもVRTは有効です。

弊フロントエンドではページ単位でのVRT実施のためにContainer/Presentationalパターンを用いず、API通信等の副作用モックとしてMock Service Workerを導入して以下のように実装しています。

const mocks = [
  (value?: any) =>
    rest.get('http://localhost:6006/api/...', (_req, res, ctx) => {
      return res(
        ctx.json(...)
      )
    })
]

export default {
  title: 'Pages/PageA',
  component: PageA
} as Meta

const Template: Story = arg => {
  const worker = getWorker()
  mocks.forEach(mock => worker.use(mock(arg)))
  return {
    components: {PageA},
    template: `<PageA />`
  }
}

上記Storyファイルに対してコミット時にVRTを実施するCIを組んでいるので、開発者は事前に意図せぬ挙動を検知することができます。また、ページ単位でStoryを作っておくとQA段階においてステージング環境に上げずとも手軽にデザイナーやPdMがUIの挙動を確認できるという副次的なメリットもあります。

これら一連の取り組みが、弊フロントエンドチームにおいて安全かつスピーディーなデザインシステムに準拠したコンポーネント駆動UI開発を支えています。

今後の課題と展望

デザインシステムは一度構築してしまえば終わりではなく、プロダクトと同様育てていくものだと考えています。

Faniconのデザインシステムは殆どアクセシビリティ(以下A11Y)対応ができていないのが現状です。Storybookにstorybook-addon-a11yというアドオンを追加し、VRT同様A11YのテストもCIで回していますが、殆どのテストが落ちたままになっています。

新規機能開発と並行してデザインシステム基盤を構築しているので手が回っていないというのもありますが、意図的に放置しているものもあります。

例えばA11Yの観点では文字と背景色は4.5:1のコントラスト比を保たなければならないと言われていますが、現状FaniconのPrimaryカラーはこの基準を満たしていません。

今この問題を対処していない理由として、既存Page-basedなページとデザインシステム+コンポーネント指向に準拠したページで不整合が起きてしまうからです。現状のFaniconは殆どのページがPage-basedで構成されているので、7割くらいのページを置き換えたタイミングで諸々に対処していこうとデザイナーのムラキ氏と話しています。

これらの問題に斬新的に対処しながらデザインシステムを育てていくことが、今後への展望と言えるでしょう。

終わりに

大変長くなりましたが、最後まで読んでいただき誠に有難うございます。

最後に触れておかなければならないのは、これら一連の取り組みを実現するにあたり欠かせなかった要素として、①コンポーネント指向かつスタイルガイドの制定を推進できる凄腕デザイナームラキ氏の精力的な協力があったこと。②2人目のフロントエンド開発者であるTomoya氏が入社したこと。③CTOやPdMを始めとし、周りの方々がフロントエンドの刷新に協力的であったこと、が挙げられます。

恐らくどれか1つでも欠けていればこの取り組みは頓挫に終わっていたと思います。

現在弊フロントエンドチームでは、本記事で紹介した取り組みに加え他にも様々な取り組みを行なっています。その取り組みの全体像について、Tomoya氏のほうから11/17にて開催される「UXエンジニア・デザインエンジニアの実情語ります!」という登壇においても紹介させて頂きますので、この記事を読んで興味が湧いた方は是非そちらもご確認いただけると幸いです。

私が所属するTHECOOでは、Faniconをより良いサービスにするための仲間を募集しています。
カジュアルに情報交換も可能ですので、お気軽にご連絡くださいませ。

THECOO 採用情報
https://hrmos.co/pages/thecoo/jobs?category=1467127248286953472

THECOO 会社情報
https://thecoonotion.notion.site/THECOO-1949fca9a7f14cef81dd8c16843ba62f

脚注
  1. Reactはフレームワークではなくライブラリだという議論もありますが、ここでは便宜上フレームワークとして扱います。 ↩︎

  2. デジタル庁のデザインシステムで述べられている通り、デザインシステムの目的の1つはデザインパーツを再利用してデザインと開発を効率化し、素早く改善サイクルを回す事であり、CDDの思想と一致しています。 ↩︎

  3. フロントエンドが専任ではない、本職がサーバーサイドやAndroidの開発者もフロントエンドを触ることがあるため ↩︎

Discussion