📚

StorybookのTSXとMDX、CSF2とCSF3の違いを整理してみた

2024/12/20に公開

この記事はポート株式会社 サービス開発部 Advent Calendar 2024の14日目の記事です。

初めまして!ポート株式会社でキャリアパークの開発をしているフロントエンドエンジニアの小島です。
キャリアパークの<span style="color: rgb(77, 77, 77)">フロントエンドチームでは、デザイナーとの認識合わせやコンポーネントの実装を確認する際にStorybookを利用しています。</span>

<span style="color: rgb(77, 77, 77)">長らく利用しているStorybookがv6だったため、今回最新版であるv8にバージョンアップ対応することになりました。</span>
しかし、v6からv8は変更点が多く、私自身Storybookの知見があまり無かったこともあり、色々な疑問点や曖昧なところが出てきたので、今回バージョンアップするに当たり調べた内容を整理します。

Storybookとは

https://storybook.js.org/

Storybookは、UIコンポーネントを開発・テスト・ドキュメント化するためのツールです。ReactやVueなど、さまざまなフレームワークに対応しています。

特徴とメリット

  • コンポーネントを分離して開発・テストできるため、アプリ全体に影響を与えることなく作業可能。バグの早期発見につながる
  • 非エンジニアともUIを共有できる
  • 開発したコンポーネントをドキュメント化し、再利用性を高められる

このように、Storybookにはこのようなメリットがあるため、近年では様々なプロジェクトで広く採用されています。

Storyについて

Storybookでは、Story という単位を使って、UIコンポーネントの特定の状態を記述します。
Storyを使ってコンポーネントの様々な状態を記述することで、簡単に再現可能にし、それぞれの状態の違いを比較しやすくなります。

Buttonコンポーネントであれば、「通常状態」「ホバー状態」「クリック状態」のような異なる状態をそれぞれストーリーとして定義します。これにより、開発者は意図した動作が正しく実現されているかを簡単に検証でき、デザイナーとの認識合わせもスムーズになります。

TSXとMDXの違い

StorybookでStoryを記述する際に一般的に使用されるファイル形式として以下があります。

  • TSX(*.stories.tsx | js | ts | jsx | svelte)
    • TypeScript+JSX形式
    • 主にComponent Story Format (CSF)を用いてストーリーを定義します。
  • MDX(*.stories.mdx) ※Storybook v7まで
  • Markdown+JSX形式
  • ドキュメントとストーリーを同一ファイル内で記述できます。

v8での変更点と弊社の対応

Writing stories directly in MDX was removed in Storybook 8, and we're no longer supporting it. Please reference the previous documentation for guidance on that feature or migrate to the new format.

MDXで直接ストーリーを記述する機能はStorybook 8で削除されました。 この機能に関するガイダンスについては以前のドキュメントを参照するか、新しいフォーマットに移行してください。

https://storybook.js.org/docs/writing-docs/mdx

https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropping-support-for-storiesmdx-csf-in-mdx-format-and-mdx1-support

上記の通り、MDXに直接ストーリを記述する機能はStorybook v8では削除されました。

ただし、MDXが完全に使えなくなるわけではありません。

Storybook v8からStory定義はTSXが必須になりましたが、MDXは引き続きドキュメント用途で利用可能です。

キャリアパークでは、Storybook v6以前の初期化ツールでは*.stories.mdx形式がデフォルト生成されていたため、全てのストーリーファイルがMDXベースになっていました。

今回Storybook v8へのバージョンアップにより、「MDXでストーリーを直接定義する機能」が廃止されたため、既存のMDXファイルで記述していたストーリーをTSXファイルへ移行することになりました。

MDXの主な変更点

  • *.stories.mdxから*.mdxに変更
  • Storyを直接記述は不可
    • Storyが必要な場合はTSXを使用する。

キャリアパークの場合

  • Storyが必要な場合
    • TSXを使用
  • Storyとドキュメントが必要な場合
    • TSXMDXを併用
  • Storyが不要な場合(ドキュメントのみなど)
    • MDXを使用

キャリアパークではこのように、Storyが必要な場合はTSXを使用し、一部Storyが不要なドキュメントに関してはMDXを使用しました。

TSXとMDXを併用する場合

前項で紹介したTSXMDXを併用したハイブリッドな使い方について簡単に紹介します。
主に細かいドキュメントが必要な複雑なコンポーネントを実装する際に活用します。

  • ストーリー定義TSXで行う。
    Storyは*.stories.tsxファイルで定義する。
  • ドキュメントMDXで行う。
    デザイン指針、アクセシビリティ考慮、UIガイドラインとの関連性など、非エンジニアにも伝えたい情報を*.mdxファイルでリッチに表現する。

サンプルコード

TSXMDXを併用した場合のサンプルを紹介します。

ディレクトリ構成

components/
├─ Button/
│  ├─ Button.tsx
│  ├─ Button.stories.tsx
│  ├─ ButtonDocs.mdx

Button.stories.tsx

import { Button } from './Button';
import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof Button> = {
  component: Button,
};
export default meta;

type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Primary Button',
  },
};

export const Secondary: Story = {
  args: {
    primary: false,
    label: 'Secondary Button',
  },
};

ButtonDocs.mdx

MDXファイルを用いてドキュメントページを用意します。
Buttonのデザイン意図やアクセシビリティ対応、UIガイドラインなどをMarkdownで記述します。

import { Meta, Story } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';

<Meta title="components/Button/Docs" of={ButtonStories} />

# Button コンポーネントガイド

`Button`は、ユーザーがアクションを起こすための主要な要素です。

- **アクセシビリティ**:
  適切な`aria-label`を付与することで、スクリーンリーダー利用者にも伝わりやすくします。

- **デザインガイドライン**:
  - `primary`ボタンはブランドカラーを用いて注目を集める
  - `secondary`ボタンは控えめな色合いで、補助的な行動を示す

## 使用例

### Primary
<Story of={ButtonStories.Primary} />

### Secondary
<Story of={ButtonStories.Secondary} />

CSF2とCSF3の違い

CSFとは

https://storybook.js.org/docs/api/csf

CSFは「Component Story Format」のことで、Storybookでコンポーネントのストーリーを記述するための推奨フォーマットです。
CSFにはバージョンがあり、Storybook v7からは従来のCSF2からCSF3が推奨されるようになりました。
v8以降はCSF2の記法が一部利用できなくなり、CSF3の記法が標準となっています。

私がCSF2からCSF3に移行する際に、注目した両者の違いを紹介します。

参考:https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#decoratorfn-story-componentstory-componentstoryobj-componentstoryfn-and-componentmeta-typescript-types

CSF2

  • 記述例
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { Button } from './Button';
 
export default {
  title: 'Button',
  component: Button,
} as ComponentMeta<typeof Button>;
 
export const Primary: ComponentStory<typeof Button> = (args) => <Button {...args} />;
Primary.args = { primary: true };

引用: https://storybook.js.org/docs/api/csf#upgrading-from-csf-2-to-csf-3

  • 特徴
    • ストーリーを「関数」として定義
    • ComponentStoryComponentMetaを使用してStorybookのコンポーネントおよびストーリーの型を定義
    • argsparametersstory.argsのようにストーリー関数に紐づけて追加する

CSF3

  • 記述例
import type { Meta, StoryObj } from '@storybook/react';

import { Button } from './Button';

const meta: Meta<typeof Button> = { component: Button };

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = { args: { primary: true } };

引用: https://storybook.js.org/docs/api/csf#upgrading-from-csf-2-to-csf-3

  • 特徴
    • ストーリーを「オブジェクト」として定義
    • StoryObjMetaを使用して、Storybookのコンポーネントおよびストーリーの型を定義
    • Storyをオブジェクトリテラルで定義するため、記述量が減り、可読性・保守性が向上
    • argsやparametersもオブジェクト内に記述
    • Storybook v8以降の標準形式

どちらを使うか

結論:CSF3

既存プロジェクトで現在CSF2を使用している場合でも、CSF3へ移行すると保守性が向上し、記述もシンプルになります。Storybook v8からはCSF2の記法が一部利用できなくなってしまうので"積極的に"CSF3へ移行を検討すべきです。

まとめ

弊社では、Storybookのストーリーファイルが300以上存在していたため、MDXからtsxへの移行に時間がかかりました。しかし、この移行により保守性が向上し、コードの一貫性や可読性も改善されました。今後はそのメリットを最大限に活かし、チームで運用していきたいと思います。

Discussion