「フロントエンド開発を爆速化する3選」というテーマでアドベントカレンダー1日目挑戦する柳沢です!
はじめに
フロントエンドの開発ってバックエンドやデザインに依存する部分があり、開発速度にモロに影響が出てしまうことがあります。
そんな時でもなるべく影響を少なくし、フロントエンドの開発速度を落とすことない、むしろ開発速度を上げる方法をご紹介させていただければと思います。
ストーリーブック駆動開発
ストーリーブック駆動開発を簡単に説明すると、StoryBook上でコンポーネントやページなどを実装しよう!という開発手段です。
StoryBookをUIカタログとしてだけ使うのではなく、開発環境の一部として使っていきます。
開発時の課題
例えば入力フォームの確認画面を開発する場合って、入力フォームにデータを入れてボタンを押して確認画面へ遷移して確認。
エラーとか出たらまた最初からやり直しってので開発体験が割とよくないですよね。
入力項目が多かったら、もうそれだけでかなりの時間を使ってしまいます。
さらに認証が付いてたりしたら、かなり辛いことになってきます。
ストーリーブック駆動開発での解決
入力画面や認証をすっ飛ばして、いきなり確認画面から開発できるようにすれば開発体験はかなり良くなるのではないでしょうか。
ストーリーファイル
まずは以下のように確認画面用のStoryBookのファイルを作り、StoryBook上にConfirMationコンポーネントを定義します。
確認画面に表示するデータはPropsから取得する想定とします。
import React from "react";
import { Meta, Story } from "@storybook/react";
import Confirmation, { Props } from "./Confirmation";
export default {
title: "Components/Confirmation",
component: Confirmation,
} as Meta;
const Template: Story<Props> = (args) => <Confirmation {...args} />;
export const Default = Template.bind({});
Default.args = {
name: "山田 太郎",
email: "taro@example.com",
message: "これはテストメッセージです。",
onBack: () => alert("戻るボタンが押されました"),
onSubmit: () => alert("送信ボタンが押されました"),
};
そしてStoryBook上でConfirmationコンポーネントを開発していきます。
StoryBookでConfirmationコンポーネントの表示確認ができるので、いちいち入力画面でデータを入力する必要がなくなり増田。
サンプルコード
import React from "react";
type Props = {
name: string,
email: string,
message: string,
onBack: () => void,
onSubmit: () => void
}
export const Confirmation: React.FC<Props> = ({
name,
email,
message,
onBack,
onSubmit,
}) => {
return (
<div>
<h1>確認画面</h1>
<div>
<p>名前: {name}</p>
<p>メールアドレス: {email}</p>
<p>メッセージ: {message}</p>
</div>
<button onClick={onBack}>戻る</button>
<button onClick={onSubmit}>送信する</button>
</div>
);
};
ストーリーブック駆動開発でのメリット
シンプルなコンポーネントになる
StoryBookでAPI連携など外部データ連携するのが難しいので、依存関係を切り離す必要があり、その結果自然とシンプルなコンポーネントになっていきます
様々なコンポーネント状態をすぐに確認できる
StoryBookでは「デフォルト状態」「エラー状態」「ローディング状態」など、様々な状態をargsの中身を変えることによって簡単に切り替えることができるので、わざわざデータを用意して再現!などの手間を省くことができます。
共通コンポーネントも簡単に実装
ButtonやInputなど小さい共通で使うコンポーネントももちろんStoryBook上で開発することができます。
またStoryBookを見ればコンポーネントの有無が分かるので、よく問題として同じコンポーネントを作ってしまった!のを防ぐことができます
スキーマー駆動開発
スキーマ駆動開発を簡単に説明すると、APIの仕様を先に作りそれを基にバックエンドとフロントエンドの開発を進めよう!という開発手段です。
開発時の課題
バックエンドのAPIが完成しないとフロントエンドのAPI連携が出来ない場面をよく見受けられます。フロントエンドがバックエンドに依存されてしまっている状態になってしまっています。
スキーマ駆動開発での解決
OpenAPIなどでスキーマを定義することにより、それを基にAPIのモックサーバを立てることができ、それを使いフロントエンドの開発をバックエンドに依存することなく開発することが可能になります。
OpenAPIの定義自体はフロントエンド/バックエンド関係なく作ることができるので、より非同期に開発を進めることが可能になります。
Prismを使ってのAPIモックサーバの構築方法
OpenAPIで記述されたAPI仕様書を元にモックサーバを構築してくれるPrismを使ってモックサーバを構築していきます。
Prismのインストール
npm でグローバルに Prism をインストールします:
npm install -g @stoplight/prism-cli
モックサーバの起動
OpenAPIなどで作成したyamlファイルを指定しprismを起動します
prism mock hoge.yaml
以下のような表示が出れば成功です。
open-api-mock % prism mock openapi.json
[17:18:43] › [CLI] … awaiting Starting Prism…
[17:18:43] › [CLI] ℹ info PUT http://127.0.0.1:4010/pet
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/pet
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/pet/findByStatus
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/pet/findByTags
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/pet/-5986411814642907
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/pet/2628331979290313?name=porro&status=aut
[17:18:43] › [CLI] ℹ info DELETE http://127.0.0.1:4010/pet/-891947936087379
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/pet/6950703153878973/uploadImage
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/store/inventory
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/store/order
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/store/order/-7827883463703699
[17:18:43] › [CLI] ℹ info DELETE http://127.0.0.1:4010/store/order/8909227023975805
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/user
[17:18:43] › [CLI] ℹ info POST http://127.0.0.1:4010/user/createWithList
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/user/login
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/user/logout
[17:18:43] › [CLI] ℹ info GET http://127.0.0.1:4010/user/neque
[17:18:43] › [CLI] ℹ info PUT http://127.0.0.1:4010/user/maiores
[17:18:43] › [CLI] ℹ info DELETE http://127.0.0.1:4010/user/ducimus
[17:18:43] › [CLI] ▶ start Prism is listening on http://127.0.0.1:4010
あとは上記エンドポイントに接続すればモックデータを取得することが可能となります。
スキーマ駆動開発でのメリット
API通信が高速になる
モックサーバへの接続になるので、通信にかかる時間がなくなりデータ取得などが高速で実現できます。
様々なコンポーネント状態をすぐに確認できる
OpenAPIのレスポンスを変更すれば、色々なパターンのレスポンスを再現することが可能で、フロント側でデータを準備する必要がなくなります
テストで動作を保証
開発時の課題
例えばXのポストに投稿時間が表示されていますが、仕様としてはザクっと以下のようになっています。
- 1時間以内なら n分 で表示
- 1時間以上24時間以内なら n時間 で表示
- 24時間以上1年以内なら MM/DD で表示
- 1年以上なら yyyy/MM/DD で表示
これをフロント側で実装時に確認するのはなかなか難しいです。
テストでの解決
モックサーバの実装でもこれは解決できますが、ユニットテストでも解決は可能になります。
まずはビジネスロジック部分だけ別関数として書き出します。
import { format, differenceInMinutes, differenceInHours, differenceInDays, differenceInYears } from 'date-fns';
export function formatPostTime(date: Date): string {
const now = new Date();
const minutesDiff = differenceInMinutes(now, date);
if (minutesDiff < 60) {
return `${minutesDiff}分`;
}
const hoursDiff = differenceInHours(now, date);
if (hoursDiff < 24) {
return `${hoursDiff}時間`;
}
const daysDiff = differenceInDays(now, date);
if (daysDiff < 365) {
return format(date, 'MM/dd');
}
return format(date, 'yyyy/MM/dd');
}
そしてこの関数に対して以下のように仕様を担保するテストを書きます。
import { describe, it, expect } from 'vitest';
import { formatPostTime } from './path/to/formatPostTime';
describe('formatPostTime 関数のテスト', () => {
it('1時間未満の場合は "n分" を返す', () => {
const now = new Date();
const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000); // 10分前
expect(formatPostTime(tenMinutesAgo)).toBe('10分');
});
it('1時間以上24時間未満の場合は "n時間" を返す', () => {
const now = new Date();
const fiveHoursAgo = new Date(now.getTime() - 5 * 60 * 60 * 1000); // 5時間前
expect(formatPostTime(fiveHoursAgo)).toBe('5時間');
});
it('24時間以上1年未満の場合は "MM/DD" を返す', () => {
const now = new Date();
const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); // 10日前
const formattedDate = `${(tenDaysAgo.getMonth() + 1).toString().padStart(2, '0')}/${tenDaysAgo
.getDate()
.toString()
.padStart(2, '0')}`;
expect(formatPostTime(tenDaysAgo)).toBe(formattedDate);
});
it('1年以上の場合は "yyyy/MM/dd" を返す', () => {
const now = new Date();
const fourHundredDaysAgo = new Date(now.getTime() - 400 * 24 * 60 * 60 * 1000); // 400日前
const formattedDate = `${fourHundredDaysAgo.getFullYear()}/${(fourHundredDaysAgo.getMonth() + 1)
.toString()
.padStart(2, '0')}/${fourHundredDaysAgo.getDate().toString().padStart(2, '0')}`;
expect(formatPostTime(fourHundredDaysAgo)).toBe(formattedDate);
});
});
これにより、ひとまずの使用を担ったコードは作成できました。
これでわざわざデータを用意する必要もなくなりました。
もちろん結合テストではちゃんとデータを用意し、テストすることは必要です。
最後に
この記事では「ストーリーブック駆動開発」「スキーマ駆動開発」、そして「テストによる動作保証」という3つの方法を通じて、フロントエンド開発を爆速化するアプローチを紹介しました。それぞれの方法が持つメリットを活用することで、開発体験が大きく向上し、効率的なプロジェクト進行が可能になります。
この記事が少しでも皆さんの開発に役立つヒントになれば幸いです!もしこの記事を読んで試してみたことや、他にもおすすめの開発手法があれば、ぜひコメントやフィードバックで教えてください。
最後までお読みいただき、ありがとうございました!