Storybook のモックデータ管理最適解

Storybook でコンポーネント開発をしていると、必ず直面するのがモックデータの管理です。美しい UI コンポーネントを作っても、適切なデータがなければその真価を発揮できません。
実際の開発現場では、API レスポンスの待機、データの不整合、環境依存の問題など、モックデータに関連する課題が山積みになっています。この記事では、そんな課題を解決し、効率的で保守性の高いモックデータ管理システムを構築する方法をご紹介します。
Storybook におけるモックデータの重要性
Storybook は、コンポーネントを独立した環境で開発・テストできる素晴らしいツールです。しかし、その真価を最大限に引き出すためには、適切なモックデータが不可欠です。
モックデータがもたらす効果
モックデータを適切に管理することで、以下のような効果が期待できます:
- 開発効率の向上: API レスポンスを待たずに即座にコンポーネントを確認
- エッジケースの検証: 異常値やエラー状態のテストが容易
- デザインの一貫性: 統一されたデータでデザインシステムの検証
- チーム協業の促進: 共通のモックデータで認識合わせがスムーズ
実際の開発現場では、以下のような状況が頻繁に発生します:
typescript// よくある問題の例
const UserProfile = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// APIが遅い、または失敗する可能性
fetchUserData()
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// 開発中は常にloading状態で、実際のUIが確認できない
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name}</div>;
};
このような状況では、開発者は実際のコンポーネントの見た目や動作を確認できず、開発効率が大幅に低下してしまいます。
モックデータ管理の課題と問題点
Storybook でのモックデータ管理には、いくつかの重要な課題が存在します。これらの課題を理解することで、適切な解決策を見つけることができます。
よくある課題とその影響
1. データの散在化
typescript// Story1.tsx
export const Default = () => (
<UserCard
user={{
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
}}
/>
);
// Story2.tsx
export const WithAvatar = () => (
<UserCard
user={{
id: 2,
name: '佐藤花子',
email: 'sato@example.com',
avatar: '/avatar.jpg',
}}
/>
);
// Story3.tsx
export const Loading = () => (
<UserCard
user={{
id: 3,
name: '山田次郎',
email: 'yamada@example.com',
}}
/>
);
このように、同じようなデータが各 Story ファイルに散在していると、データの更新や管理が困難になります。
2. 型安全性の欠如
typescript// 型定義がないモックデータ
const mockUser = {
id: 1,
name: 'テストユーザー',
// 実際のAPIレスポンスと異なる構造
profile: {
age: 25,
location: '東京',
},
};
// 実際のAPIレスポンス
interface User {
id: number;
name: string;
email: string;
profile: {
age: number;
location: string;
bio?: string; // 必須フィールドが不足
};
}
型定義とモックデータが一致していないと、実際の環境との差異が生まれ、開発時の混乱を招きます。
3. 環境依存の問題
typescript// 開発環境でのみ動作するモック
const mockApiResponse = {
users: [{ id: 1, name: '開発用ユーザー' }],
};
// 本番環境では異なる構造
const productionApiResponse = {
data: {
users: [
{
id: 1,
name: '本番ユーザー',
createdAt: '2024-01-01',
},
],
},
meta: {
total: 1,
page: 1,
},
};
環境によって API レスポンスの構造が異なる場合、モックデータが実際の環境と乖離してしまいます。
4. パフォーマンスの問題
typescript// 大量のデータを毎回生成
const generateLargeDataset = () => {
return Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `ユーザー${i}`,
email: `user${i}@example.com`,
// 大量の不要なデータ
metadata: {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
// ... その他の大量データ
},
}));
};
不適切なモックデータは、Storybook の起動時間やメモリ使用量に悪影響を与えます。
最適なモックデータ管理手法
これらの課題を解決するために、体系的なモックデータ管理手法を実装しましょう。まずは基本的な構造から始めて、段階的に改善していきます。
モックデータの階層構造
効率的なモックデータ管理のため、以下の階層構造を採用します:
typescript// src/mocks/types.ts
// 型定義を一元管理
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
profile: {
age: number;
location: string;
bio?: string;
};
createdAt: string;
updatedAt: string;
}
export interface ApiResponse<T> {
data: T;
meta: {
total: number;
page: number;
limit: number;
};
}
typescript// src/mocks/factories/userFactory.ts
// ファクトリーパターンでモックデータを生成
import { faker } from '@faker-js/faker/locale/ja';
import { User } from '../types';
export class UserFactory {
static create(overrides: Partial<User> = {}): User {
return {
id: faker.number.int({ min: 1, max: 1000 }),
name: faker.person.fullName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
profile: {
age: faker.number.int({ min: 18, max: 80 }),
location: faker.location.city(),
bio: faker.lorem.sentence(),
},
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
...overrides,
};
}
static createMany(
count: number,
overrides: Partial<User> = {}
): User[] {
return Array.from({ length: count }, () =>
this.create(overrides)
);
}
}
エラー状態のモックデータ
実際の開発では、エラー状態のテストも重要です。以下のようにエラーケースも含めたモックデータを作成します:
typescript// src/mocks/scenarios/errorScenarios.ts
// エラーケースのシナリオを定義
export const errorScenarios = {
networkError: {
status: 500,
message: 'Internal Server Error',
code: 'NETWORK_ERROR',
},
validationError: {
status: 400,
message: 'Invalid input data',
code: 'VALIDATION_ERROR',
details: {
email: 'Invalid email format',
name: 'Name is required',
},
},
notFoundError: {
status: 404,
message: 'User not found',
code: 'NOT_FOUND',
},
};
MSW(Mock Service Worker)を活用した統合管理
MSW は、API モックのための強力なツールです。Storybook と組み合わせることで、実際の API コールを完全にモック化できます。
MSW のセットアップ
まず、MSW をプロジェクトにインストールします:
bashyarn add -D msw
typescript// src/mocks/handlers.ts
// APIハンドラーを定義
import { http, HttpResponse } from 'msw';
import { UserFactory } from './factories/userFactory';
export const handlers = [
// ユーザー一覧取得
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const page = parseInt(
url.searchParams.get('page') || '1'
);
const limit = parseInt(
url.searchParams.get('limit') || '10'
);
const users = UserFactory.createMany(limit);
return HttpResponse.json({
data: users,
meta: {
total: 100,
page,
limit,
},
});
}),
// 個別ユーザー取得
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
if (id === '999') {
return new HttpResponse(null, {
status: 404,
statusText: 'Not Found',
});
}
const user = UserFactory.create({ id: Number(id) });
return HttpResponse.json({ data: user });
}),
// ユーザー作成
http.post('/api/users', async ({ request }) => {
const body = await request.json();
if (!body.name || !body.email) {
return new HttpResponse(
JSON.stringify({
message: 'Validation failed',
errors: {
name: body.name
? undefined
: 'Name is required',
email: body.email
? undefined
: 'Email is required',
},
}),
{ status: 400 }
);
}
const newUser = UserFactory.create(body);
return HttpResponse.json(
{ data: newUser },
{ status: 201 }
);
}),
];
Storybook での MSW 統合
typescript// .storybook/preview.ts
// StorybookでMSWを有効化
import { initialize, mswLoader } from 'msw-storybook-addon';
// MSWの初期化
initialize();
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const loaders = [mswLoader];
typescript// src/stories/UserCard.stories.tsx
// MSWを使用したStory
import type { Meta, StoryObj } from '@storybook/react';
import { UserCard } from '../components/UserCard';
import { handlers } from '../mocks/handlers';
const meta: Meta<typeof UserCard> = {
title: 'Components/UserCard',
component: UserCard,
parameters: {
msw: {
handlers: handlers,
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
userId: 1,
},
};
export const Loading: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users/:id', () => {
return HttpResponse.delay(2000);
}),
],
},
},
args: {
userId: 1,
},
};
export const Error: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 404 });
}),
],
},
},
args: {
userId: 999,
},
};
Storybook 専用モックライブラリの活用
MSW に加えて、Storybook 専用のモックライブラリを活用することで、より柔軟で強力なモックシステムを構築できます。
Storybook Addon MSW
typescript// .storybook/main.ts
// Storybookの設定にMSWアドオンを追加
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
'msw-storybook-addon', // MSWアドオンを追加
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
カスタムモックアドオンの作成
typescript// src/mocks/addons/mockDataAddon.ts
// カスタムモックアドオン
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Mock Data',
match: ({ viewMode }) => viewMode === 'story',
render: () => {
// モックデータの管理パネルを実装
return document.createElement('div');
},
});
});
動的モックデータの生成
typescript// src/mocks/utils/dynamicMockGenerator.ts
// 動的にモックデータを生成するユーティリティ
export class DynamicMockGenerator {
static generateUserData(
scenario: 'normal' | 'error' | 'loading' = 'normal'
) {
switch (scenario) {
case 'normal':
return UserFactory.create();
case 'error':
throw new Error('User data generation failed');
case 'loading':
return new Promise((resolve) =>
setTimeout(
() => resolve(UserFactory.create()),
2000
)
);
default:
return UserFactory.create();
}
}
static generateListData(
count: number,
filters?: Record<string, any>
) {
let users = UserFactory.createMany(count);
if (filters) {
users = users.filter((user) => {
return Object.entries(filters).every(
([key, value]) => {
return user[key as keyof typeof user] === value;
}
);
});
}
return users;
}
}
環境別モックデータの使い分け
開発環境、テスト環境、本番環境など、環境によって異なるモックデータが必要な場合があります。効率的に管理する方法をご紹介します。
環境別設定の管理
typescript// src/mocks/config/environment.ts
// 環境別の設定を管理
export const mockConfig = {
development: {
apiDelay: 100,
errorRate: 0.1,
dataSize: 'small',
},
test: {
apiDelay: 0,
errorRate: 0,
dataSize: 'minimal',
},
staging: {
apiDelay: 500,
errorRate: 0.05,
dataSize: 'medium',
},
production: {
apiDelay: 1000,
errorRate: 0.02,
dataSize: 'large',
},
};
export const getCurrentEnvironment = () => {
return process.env.NODE_ENV || 'development';
};
export const getMockConfig = () => {
const env = getCurrentEnvironment();
return (
mockConfig[env as keyof typeof mockConfig] ||
mockConfig.development
);
};
環境別ハンドラーの実装
typescript// src/mocks/handlers/environmentHandlers.ts
// 環境別のハンドラーを実装
import { http, HttpResponse } from 'msw';
import { getMockConfig } from '../config/environment';
export const createEnvironmentHandlers = () => {
const config = getMockConfig();
return [
http.get('/api/users', async () => {
// 環境に応じた遅延を追加
if (config.apiDelay > 0) {
await new Promise((resolve) =>
setTimeout(resolve, config.apiDelay)
);
}
// 環境に応じたエラー率を設定
if (Math.random() < config.errorRate) {
return new HttpResponse(null, { status: 500 });
}
const dataSize =
config.dataSize === 'large'
? 100
: config.dataSize === 'medium'
? 50
: 10;
const users = UserFactory.createMany(dataSize);
return HttpResponse.json({
data: users,
meta: {
total: dataSize,
page: 1,
limit: dataSize,
},
});
}),
];
};
Storybook での環境切り替え
typescript// .storybook/preview.ts
// 環境別の設定をStorybookに適用
import { createEnvironmentHandlers } from '../src/mocks/handlers/environmentHandlers';
export const parameters = {
msw: {
handlers: createEnvironmentHandlers(),
},
// 環境変数をStorybookで利用可能にする
env: {
NODE_ENV: process.env.NODE_ENV || 'development',
},
};
パフォーマンス最適化のテクニック
モックデータの管理において、パフォーマンスは重要な要素です。効率的で軽量なモックシステムを構築する方法をご紹介します。
メモ化による最適化
typescript// src/mocks/utils/memoizedMockData.ts
// メモ化によるパフォーマンス最適化
class MockDataCache {
private cache = new Map<string, any>();
private maxSize = 100;
get(key: string) {
return this.cache.get(key);
}
set(key: string, value: any) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
}
const mockCache = new MockDataCache();
export const getMemoizedUserData = (userId: number) => {
const cacheKey = `user_${userId}`;
let userData = mockCache.get(cacheKey);
if (!userData) {
userData = UserFactory.create({ id: userId });
mockCache.set(cacheKey, userData);
}
return userData;
};
遅延ローディングの実装
typescript// src/mocks/utils/lazyMockData.ts
// 遅延ローディングによる最適化
export class LazyMockDataLoader {
private static instance: LazyMockDataLoader;
private loadedData = new Set<string>();
static getInstance() {
if (!this.instance) {
this.instance = new LazyMockDataLoader();
}
return this.instance;
}
async loadUserData(userId: number) {
const key = `user_${userId}`;
if (this.loadedData.has(key)) {
return getMemoizedUserData(userId);
}
// 実際のAPIコールをシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
const userData = UserFactory.create({ id: userId });
this.loadedData.add(key);
return userData;
}
preloadUserData(userIds: number[]) {
return Promise.all(
userIds.map((id) => this.loadUserData(id))
);
}
}
バッチ処理の最適化
typescript// src/mocks/handlers/optimizedHandlers.ts
// バッチ処理による最適化
import { http, HttpResponse } from 'msw';
import { LazyMockDataLoader } from '../utils/lazyMockData';
export const optimizedHandlers = [
// バッチでユーザーデータを取得
http.post('/api/users/batch', async ({ request }) => {
const { userIds } = await request.json();
const loader = LazyMockDataLoader.getInstance();
const users = await loader.preloadUserData(userIds);
return HttpResponse.json({
data: users,
meta: {
total: users.length,
batchSize: userIds.length,
},
});
}),
// ページネーション最適化
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const page = parseInt(
url.searchParams.get('page') || '1'
);
const limit = parseInt(
url.searchParams.get('limit') || '10'
);
// 必要な分だけデータを生成
const startIndex = (page - 1) * limit;
const users = UserFactory.createMany(limit).map(
(user, index) => ({
...user,
id: startIndex + index + 1,
})
);
return HttpResponse.json({
data: users,
meta: {
total: 1000,
page,
limit,
hasNext: page * limit < 1000,
hasPrev: page > 1,
},
});
}),
];
チーム開発でのモックデータ共有戦略
チーム開発では、モックデータの共有と標準化が重要です。効率的な共有戦略を実装しましょう。
モックデータのバージョン管理
typescript// src/mocks/versioning/mockVersion.ts
// モックデータのバージョン管理
export interface MockVersion {
version: string;
schema: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export class MockVersionManager {
private static currentVersion = '1.0.0';
static getCurrentVersion(): string {
return this.currentVersion;
}
static validateMockData(data: any, schema: any): boolean {
// スキーマ検証の実装
return true;
}
static migrateMockData(
data: any,
fromVersion: string,
toVersion: string
) {
// バージョン間のデータ移行ロジック
return data;
}
}
チーム共有用のモックライブラリ
typescript// src/mocks/shared/teamMockLibrary.ts
// チーム共有用のモックライブラリ
export class TeamMockLibrary {
private static sharedMocks = new Map<string, any>();
static registerMock(name: string, mockData: any) {
this.sharedMocks.set(name, {
data: mockData,
createdAt: new Date().toISOString(),
createdBy: process.env.USER || 'unknown',
});
}
static getMock(name: string) {
const mock = this.sharedMocks.get(name);
if (!mock) {
throw new Error(`Mock "${name}" not found`);
}
return mock.data;
}
static listMocks() {
return Array.from(this.sharedMocks.keys());
}
static exportMocks() {
return Object.fromEntries(this.sharedMocks);
}
static importMocks(mocks: Record<string, any>) {
Object.entries(mocks).forEach(([name, mock]) => {
this.registerMock(name, mock.data);
});
}
}
モックデータのドキュメント化
typescript// src/mocks/docs/mockDocumentation.ts
// モックデータのドキュメント化
export interface MockDocumentation {
name: string;
description: string;
schema: Record<string, any>;
examples: any[];
usage: string;
tags: string[];
}
export class MockDocumentationGenerator {
static generateDocs(
mockName: string,
mockData: any
): MockDocumentation {
return {
name: mockName,
description: `Mock data for ${mockName}`,
schema: this.inferSchema(mockData),
examples: [mockData],
usage: this.generateUsageExample(mockName),
tags: this.extractTags(mockData),
};
}
private static inferSchema(
data: any
): Record<string, any> {
// スキーマ推論ロジック
return {};
}
private static generateUsageExample(
mockName: string
): string {
return `
// 使用方法
import { TeamMockLibrary } from './teamMockLibrary';
const mockData = TeamMockLibrary.getMock('${mockName}');
`.trim();
}
private static extractTags(data: any): string[] {
// タグ抽出ロジック
return ['user', 'profile'];
}
}
CI/CD でのモックデータ検証
yaml# .github/workflows/mock-validation.yml
# CI/CDでのモックデータ検証
name: Mock Data Validation
on:
pull_request:
paths:
- 'src/mocks/**'
jobs:
validate-mocks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Validate mock data
run: yarn validate:mocks
- name: Run Storybook tests
run: yarn test:storybook
typescript// scripts/validateMocks.ts
// モックデータ検証スクリプト
import { MockVersionManager } from '../src/mocks/versioning/mockVersion';
import { TeamMockLibrary } from '../src/mocks/shared/teamMockLibrary';
async function validateMocks() {
const mocks = TeamMockLibrary.listMocks();
let hasErrors = false;
for (const mockName of mocks) {
try {
const mockData = TeamMockLibrary.getMock(mockName);
// スキーマ検証
if (
!MockVersionManager.validateMockData(mockData, {})
) {
console.error(
`❌ Mock "${mockName}" failed schema validation`
);
hasErrors = true;
}
// 型安全性チェック
if (!validateTypeSafety(mockData)) {
console.error(
`❌ Mock "${mockName}" failed type safety check`
);
hasErrors = true;
}
console.log(
`✅ Mock "${mockName}" validated successfully`
);
} catch (error) {
console.error(
`❌ Error validating mock "${mockName}":`,
error
);
hasErrors = true;
}
}
if (hasErrors) {
process.exit(1);
}
console.log('🎉 All mocks validated successfully!');
}
function validateTypeSafety(data: any): boolean {
// 型安全性チェックの実装
return true;
}
validateMocks();
まとめ
Storybook でのモックデータ管理は、一見複雑に見えますが、適切なアプローチを取ることで、効率的で保守性の高いシステムを構築できます。
この記事でご紹介した手法を実践することで、以下のような効果が期待できます:
- 開発効率の大幅な向上: 適切なモックデータにより、API レスポンスを待たずにコンポーネントの開発・テストが可能
- 品質の向上: エッジケースやエラー状態のテストが容易になり、バグの早期発見が可能
- チーム協業の促進: 統一されたモックデータにより、認識合わせがスムーズになり、開発速度が向上
- 保守性の向上: 体系的なモックデータ管理により、長期的なメンテナンスが容易
特に重要なのは、モックデータを「一時的なもの」として扱うのではなく、「開発プロセスの重要な一部」として位置づけることです。適切に設計されたモックシステムは、プロジェクトの成功に大きく貢献します。
実際の開発現場では、これらの手法を段階的に導入し、チームの状況に合わせてカスタマイズしていくことをお勧めします。最初は小さく始めて、徐々に改善していくことで、無理なく効果的なモックデータ管理システムを構築できます。
モックデータ管理の最適化は、Storybook の真価を最大限に引き出すための重要な要素です。この記事でご紹介した手法を参考に、あなたのプロジェクトに最適なモックデータ管理システムを構築してください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来