T-CREATOR

Storybook で UI バグを未然に防ぐチェックリスト

Storybook で UI バグを未然に防ぐチェックリスト

UI 開発において、見た目の美しさだけでなく、安定性や使いやすさも重要な要素です。Storybook は、コンポーネントの開発・テスト・ドキュメント化を効率化するツールとして多くの開発者に愛用されています。

しかし、ただ Storybook を導入するだけでは十分ではありません。適切なチェックリストに従って運用することで、UI バグを未然に防ぎ、高品質なユーザー体験を提供できるようになります。

本記事では、実際の開発現場で使える実践的なチェックリストをご紹介します。コードエラーの対処法から、見落としがちなバグの発見方法まで、明日から使える知識をお届けいたします。

背景

UI 開発で発生しがちなバグの種類

UI 開発では、様々な種類のバグが発生します。特に頻繁に遭遇するのは以下のようなバグです。

#バグの種類発生頻度影響度
1レイアウト崩れ
2状態管理エラー
3プロパティ型エラー
4レスポンシブ対応不備
5アクセシビリティ問題

これらのバグは、ユーザー体験を大きく損なう可能性があります。特に状態管理エラーやプロパティ型エラーは、アプリケーションの動作を停止させる可能性があるため、早期発見が重要です。

Storybook がバグ防止に果たす役割

Storybook は、コンポーネントを独立した環境で開発・テストできる環境を提供します。この特性により、以下のような効果が期待できます。

独立したテスト環境の提供 コンポーネントを他の要素から切り離してテストできるため、バグの原因を特定しやすくなります。

視覚的な確認の効率化 様々な状態やプロパティの組み合わせを一度に確認できるため、手動テストでは見落としがちなバグを発見できます。

ドキュメント化の自動化 コンポーネントの使用方法を明確にすることで、誤った使用によるバグを防げます。

課題

従来の UI 開発で見落としがちなバグ要因

従来の UI 開発では、以下のような要因でバグが見落とされることがあります。

複雑な状態の組み合わせ 実際のアプリケーションでは、コンポーネントが様々な状態の組み合わせで表示されます。しかし、開発時には限られた状態でのみテストが行われることが多く、レアケースでのバグが見落とされがちです。

ブラウザ間の差異 異なるブラウザでの表示確認が不十分だと、特定のブラウザでのみ発生するバグが本番環境で発覚することがあります。

デバイスサイズの多様性 スマートフォンからデスクトップまで、様々なデバイスサイズでの表示確認が必要ですが、限られたデバイスでのみテストが行われることが多いです。

手動テストの限界と問題点

手動テストには以下のような限界があります。

時間コストの問題 すべての状態や組み合わせを手動で確認するには、膨大な時間が必要です。

人的エラーの可能性 同じテストを繰り返し実行する際に、見落としや確認漏れが発生する可能性があります。

再現性の問題 バグが発生した際の状況を正確に再現することが困難な場合があります。

解決策

Storybook を活用したバグ防止アプローチ

Storybook を活用することで、これらの課題を効果的に解決できます。

システマティックなテスト チェックリストに従って体系的にテストを実行することで、見落としを防げます。

自動化との組み合わせ Storybook のアドオンを活用して、自動化可能なテストを導入できます。

チーム全体での品質向上 共通のチェックリストを使用することで、チーム全体での品質基準を統一できます。

具体例

セットアップ時のチェックリスト

Storybook を導入する際に確認すべきポイントをまとめました。

1. 必要な依存関係の確認

まず、プロジェクトに必要な依存関係をインストールします。

typescript// package.json の devDependencies に必要なパッケージを追加
{
  "devDependencies": {
    "@storybook/react": "^7.0.0",
    "@storybook/addon-essentials": "^7.0.0",
    "@storybook/addon-interactions": "^7.0.0",
    "@storybook/testing-library": "^0.2.0"
  }
}

Yarn を使用してパッケージをインストールします。

bashyarn add --dev @storybook/react @storybook/addon-essentials @storybook/addon-interactions @storybook/testing-library

2. TypeScript 設定の確認

TypeScript プロジェクトの場合、型定義が正しく設定されているか確認します。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) =>
        prop.parent
          ? !/node_modules/.test(prop.parent.fileName)
          : true,
    },
  },
};

export default config;

3. 環境変数の設定確認

開発環境と Storybook で環境変数の設定が一致しているか確認します。

typescript// .storybook/preview.ts
import type { Preview } from '@storybook/react';

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;

4. 基本的なエラーハンドリング

Storybook でよく発生するエラーとその対処法を確認します。

bash# よく発生するエラー1: モジュールが見つからない
Error: Cannot resolve module 'react-dom/client'

このエラーは、React18 以降で発生することがあります。解決方法は以下の通りです。

typescript// .storybook/main.ts に以下を追加
const config: StorybookConfig = {
  // ... 他の設定
  core: {
    builder: '@storybook/builder-vite',
  },
  viteFinal: async (config) => {
    config.resolve = config.resolve || {};
    config.resolve.alias = {
      ...config.resolve.alias,
      'react-dom/client': 'react-dom/client',
    };
    return config;
  },
};

Story 作成時のチェックリスト

各コンポーネントの Story を作成する際に確認すべきポイントです。

1. 基本的な Story の構造確認

まず、Story の基本構造が正しく設定されているか確認します。

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

const meta: Meta<typeof Button> = {
  title: 'Example/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

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

2. 必須の Story バリエーション

最低限、以下の Story を作成することを推奨します。

typescript// 基本的な使用例
export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

// セカンダリー使用例
export const Secondary: Story = {
  args: {
    label: 'Button',
  },
};

// サイズバリエーション
export const Large: Story = {
  args: {
    size: 'large',
    label: 'Button',
  },
};

export const Small: Story = {
  args: {
    size: 'small',
    label: 'Button',
  },
};

3. エラー状態の Story

エラー状態やローディング状態も含めて Story を作成します。

typescript// エラー状態のStory
export const ErrorState: Story = {
  args: {
    label: 'Error Button',
    error: true,
  },
};

// ローディング状態のStory
export const LoadingState: Story = {
  args: {
    label: 'Loading...',
    loading: true,
    disabled: true,
  },
};

4. 長いテキストの対応確認

文字数が多い場合の表示確認も重要です。

typescript// 長いテキストのStory
export const LongText: Story = {
  args: {
    label:
      'This is a very long button text that might cause layout issues',
  },
};

// 極端に短いテキストのStory
export const ShortText: Story = {
  args: {
    label: 'A',
  },
};

プロパティ検証のチェックリスト

TypeScript の型定義と Proptypes を活用した検証方法を確認します。

1. TypeScript 型定義の確認

まず、コンポーネントの型定義が適切に設定されているか確認します。

typescript// Button.tsx
import React from 'react';

interface ButtonProps {
  /**
   * ボタンの種類を指定します
   */
  primary?: boolean;
  /**
   * ボタンのサイズを指定します
   */
  size?: 'small' | 'medium' | 'large';
  /**
   * ボタンのラベルテキストを指定します
   */
  label: string;
  /**
   * クリック時のイベントハンドラー
   */
  onClick?: () => void;
  /**
   * ボタンの無効化状態
   */
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  primary = false,
  size = 'medium',
  label,
  disabled = false,
  ...props
}) => {
  const mode = primary ? 'primary' : 'secondary';
  return (
    <button
      type='button'
      className={`button button--${size} button--${mode}`}
      disabled={disabled}
      {...props}
    >
      {label}
    </button>
  );
};

2. 型エラーの確認

よく発生する型エラーとその対処法を確認します。

typescript// エラー例: Property 'label' is missing in type
// 以下のStoryでエラーが発生する場合
export const InvalidStory: Story = {
  args: {
    primary: true,
    // label が不足している
  },
};

この問題は、必須プロパティの指定により解決できます。

typescript// 正しい書き方
export const ValidStory: Story = {
  args: {
    primary: true,
    label: 'Valid Button', // 必須プロパティを指定
  },
};

3. Props validation in development

開発環境でのプロパティ検証を有効化します。

typescript// Button.tsx に以下を追加(開発時のみ)
import PropTypes from 'prop-types';

Button.propTypes = {
  primary: PropTypes.bool,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func,
  disabled: PropTypes.bool,
};

4. 無効なプロパティの確認

無効なプロパティが渡された場合の動作を確認します。

typescript// 無効なプロパティのテスト用Story
export const InvalidProps: Story = {
  args: {
    // @ts-expect-error: 無効なサイズを意図的に指定
    size: 'extra-large',
    label: 'Invalid Size Button',
  },
};

状態管理のチェックリスト

コンポーネントの状態管理が適切に行われているか確認します。

1. useState の適切な使用

状態の更新が正しく行われているか確認します。

typescript// カウンターコンポーネントの例
import React, { useState } from 'react';

interface CounterProps {
  initialValue?: number;
  step?: number;
}

export const Counter: React.FC<CounterProps> = ({
  initialValue = 0,
  step = 1,
}) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount((prevCount) => prevCount + step);
  };

  const decrement = () => {
    setCount((prevCount) => prevCount - step);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

2. 状態の初期化確認

コンポーネントの初期状態が正しく設定されているか確認します。

typescript// Counter.stories.tsx
export const DefaultCounter: Story = {
  args: {
    initialValue: 0,
    step: 1,
  },
};

export const CustomInitialValue: Story = {
  args: {
    initialValue: 10,
    step: 2,
  },
};

3. 状態更新のテスト

Storybook のインタラクションテストを使用して、状態の更新をテストします。

typescript// インタラクションテストの例
import { expect } from '@storybook/jest';
import {
  within,
  userEvent,
} from '@storybook/testing-library';

export const InteractionTest: Story = {
  args: {
    initialValue: 0,
    step: 1,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // 初期値の確認
    const countText = canvas.getByText('Count: 0');
    expect(countText).toBeInTheDocument();

    // インクリメントボタンのクリック
    const incrementButton = canvas.getByText('+');
    await userEvent.click(incrementButton);

    // 更新後の値の確認
    const updatedCountText = canvas.getByText('Count: 1');
    expect(updatedCountText).toBeInTheDocument();
  },
};

4. 状態管理のエラーハンドリング

状態更新時のエラーを適切に処理しているか確認します。

typescript// エラーハンドリングを含むコンポーネント
export const CounterWithErrorHandling: React.FC<
  CounterProps
> = ({ initialValue = 0, step = 1 }) => {
  const [count, setCount] = useState(initialValue);
  const [error, setError] = useState<string | null>(null);

  const increment = () => {
    try {
      if (count + step > 100) {
        throw new Error('Count cannot exceed 100');
      }
      setCount((prevCount) => prevCount + step);
      setError(null);
    } catch (err) {
      setError(
        err instanceof Error
          ? err.message
          : 'An error occurred'
      );
    }
  };

  return (
    <div>
      <p>Count: {count}</p>
      {error && (
        <p style={{ color: 'red' }}>Error: {error}</p>
      )}
      <button onClick={increment}>+</button>
    </div>
  );
};

レスポンシブ対応のチェックリスト

様々なデバイスサイズでの表示確認を行います。

1. ビューポートの設定

Storybook でビューポートを設定して、異なるデバイスサイズでの表示を確認します。

typescript// .storybook/preview.ts
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';

const preview: Preview = {
  parameters: {
    viewport: {
      viewports: INITIAL_VIEWPORTS,
      defaultViewport: 'responsive',
    },
  },
};

2. レスポンシブコンポーネントの作成

メディアクエリを使用したレスポンシブコンポーネントの例です。

typescript// ResponsiveCard.tsx
import React from 'react';
import './ResponsiveCard.css';

interface ResponsiveCardProps {
  title: string;
  content: string;
  imageUrl?: string;
}

export const ResponsiveCard: React.FC<
  ResponsiveCardProps
> = ({ title, content, imageUrl }) => {
  return (
    <div className='responsive-card'>
      {imageUrl && (
        <img
          src={imageUrl}
          alt={title}
          className='responsive-card__image'
        />
      )}
      <div className='responsive-card__content'>
        <h3 className='responsive-card__title'>{title}</h3>
        <p className='responsive-card__text'>{content}</p>
      </div>
    </div>
  );
};

3. CSS メディアクエリの設定

css/* ResponsiveCard.css */
.responsive-card {
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  max-width: 400px;
  margin: 0 auto;
}

.responsive-card__image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.responsive-card__content {
  padding: 16px;
}

.responsive-card__title {
  margin: 0 0 8px 0;
  font-size: 1.2rem;
}

.responsive-card__text {
  margin: 0;
  color: #666;
  line-height: 1.4;
}

/* タブレット以上のサイズ */
@media (min-width: 768px) {
  .responsive-card {
    flex-direction: row;
    max-width: 600px;
  }

  .responsive-card__image {
    width: 200px;
    height: auto;
  }

  .responsive-card__content {
    flex: 1;
  }
}

/* デスクトップサイズ */
@media (min-width: 1024px) {
  .responsive-card {
    max-width: 800px;
  }

  .responsive-card__image {
    width: 300px;
  }

  .responsive-card__title {
    font-size: 1.5rem;
  }
}

4. デバイス別の Story 作成

異なるデバイスサイズでの表示確認用の Story を作成します。

typescript// ResponsiveCard.stories.tsx
export const Mobile: Story = {
  args: {
    title: 'Mobile Card',
    content:
      'This is how the card appears on mobile devices.',
    imageUrl: 'https://via.placeholder.com/300x200',
  },
  parameters: {
    viewport: {
      defaultViewport: 'mobile1',
    },
  },
};

export const Tablet: Story = {
  args: {
    title: 'Tablet Card',
    content:
      'This is how the card appears on tablet devices.',
    imageUrl: 'https://via.placeholder.com/300x200',
  },
  parameters: {
    viewport: {
      defaultViewport: 'tablet',
    },
  },
};

export const Desktop: Story = {
  args: {
    title: 'Desktop Card',
    content:
      'This is how the card appears on desktop devices.',
    imageUrl: 'https://via.placeholder.com/300x200',
  },
  parameters: {
    viewport: {
      defaultViewport: 'desktop',
    },
  },
};

アクセシビリティのチェックリスト

アクセシビリティを考慮したコンポーネント開発を行います。

1. アクセシビリティアドオンの設定

Storybook でアクセシビリティをチェックするためのアドオンを導入します。

typescript// .storybook/main.ts
const config: StorybookConfig = {
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-a11y', // アクセシビリティアドオン
  ],
};

2. セマンティック HTML の使用

適切な HTML 要素を使用してコンポーネントを作成します。

typescript// AccessibleButton.tsx
import React from 'react';

interface AccessibleButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  disabled?: boolean;
  ariaLabel?: string;
  ariaDescribedBy?: string;
  type?: 'button' | 'submit' | 'reset';
}

export const AccessibleButton: React.FC<
  AccessibleButtonProps
> = ({
  children,
  onClick,
  disabled = false,
  ariaLabel,
  ariaDescribedBy,
  type = 'button',
}) => {
  return (
    <button
      type={type}
      onClick={onClick}
      disabled={disabled}
      aria-label={ariaLabel}
      aria-describedby={ariaDescribedBy}
      className='accessible-button'
    >
      {children}
    </button>
  );
};

3. フォーカス管理の確認

キーボードナビゲーションが適切に動作するか確認します。

typescript// AccessibleForm.tsx
import React, { useRef } from 'react';

interface AccessibleFormProps {
  onSubmit: (data: FormData) => void;
}

export const AccessibleForm: React.FC<
  AccessibleFormProps
> = ({ onSubmit }) => {
  const nameInputRef = useRef<HTMLInputElement>(null);
  const emailInputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(
      e.currentTarget as HTMLFormElement
    );
    onSubmit(formData);
  };

  return (
    <form
      onSubmit={handleSubmit}
      className='accessible-form'
    >
      <div className='form-group'>
        <label htmlFor='name'>お名前 *</label>
        <input
          id='name'
          name='name'
          type='text'
          required
          ref={nameInputRef}
          aria-describedby='name-error'
        />
        <div
          id='name-error'
          className='error-message'
          role='alert'
        >
          {/* エラーメッセージがここに表示される */}
        </div>
      </div>

      <div className='form-group'>
        <label htmlFor='email'>メールアドレス *</label>
        <input
          id='email'
          name='email'
          type='email'
          required
          ref={emailInputRef}
          aria-describedby='email-error'
        />
        <div
          id='email-error'
          className='error-message'
          role='alert'
        >
          {/* エラーメッセージがここに表示される */}
        </div>
      </div>

      <button type='submit'>送信</button>
    </form>
  );
};

4. アクセシビリティテストの実行

アクセシビリティテストを含む Story を作成します。

typescript// AccessibleButton.stories.tsx
export const AccessibilityTest: Story = {
  args: {
    children: 'Accessible Button',
    onClick: () => console.log('Button clicked'),
    ariaLabel: 'Click this button to perform an action',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');

    // フォーカス可能かテスト
    button.focus();
    expect(document.activeElement).toBe(button);

    // キーボードで操作可能かテスト
    await userEvent.keyboard('{Enter}');

    // ARIA属性の確認
    expect(button).toHaveAttribute(
      'aria-label',
      'Click this button to perform an action'
    );
  },
};

まとめ

Storybook を活用した UI バグ防止チェックリストをご紹介しました。これらのチェックリストを活用することで、以下のような効果を期待できます。

品質の向上 体系的なチェックにより、見落としがちなバグを未然に防ぐことができます。

開発効率の向上 標準化されたプロセスにより、チーム全体での開発効率が向上します。

メンテナンス性の向上 適切にドキュメント化されたコンポーネントは、長期的な保守性を高めます。

ユーザー体験の向上 アクセシビリティやレスポンシブ対応により、すべてのユーザーにとって使いやすい UI を提供できます。

重要なのは、これらのチェックリストを形式的に実行するのではなく、プロジェクトの特性に合わせてカスタマイズして活用することです。定期的にチェックリストを見直し、新しい知見や技術を取り入れながら、継続的に改善していきましょう。

明日からでも始められる項目から取り組んで、より良い UI 開発を実現してください。

関連リンク