フロントエンドの実務
この部の 8 / 9 章 ・ 全体で 41 / 76 章 ・ 読了目安 45 分
- 状態をどこに置くかを判断できる
- 0件とエラーの画面を必ず設計できる
- デザインに無い状態を自分から洗い出せる
HTML と CSSで HTML と CSS、React で画面を組み立てるで React を見ました。 この章は、チームで、実際のサービスの画面を作る時に必要になることです。
フロントエンドには、サーバー側とは違う難しさがあります。
□ 見た目そのものが仕様であり、言葉で伝わりにくい
□ 実行環境を選べない(ブラウザ・OS・画面サイズ・回線速度がバラバラ)
□ 状態が多い(読み込み中・エラー・空・部分的に成功)
□ 変更頻度が高い(デザイン変更・A/B テスト・文言修正)
「画面を作るだけ」に見えて、考えることの数はサーバー側と変わりません。
ビルドの仕組みを知っておく
書いたコードは、そのままブラウザで動くわけではありません。
TypeScript / JSX / CSS
↓ トランスパイル(古いブラウザでも動く形へ)
↓ バンドル(多数のファイルを結合し、依存を解決)
↓ 最適化(未使用コードの削除、圧縮、ハッシュ付きファイル名)
配信される JS / CSS
なぜこれを知る必要があるか。
□ 「手元では動くのに本番で表示が崩れる」→ ビルド設定の差
□ 「ページが重い」→ バンドルサイズを見る
□ 「変更が反映されない」→ キャッシュとハッシュ(性能と負荷対策)
□ 環境変数がビルド時に埋め込まれる → 秘密情報を入れてはいけない
NEXT_PUBLIC_API_KEY=xxxxx ← ビルド結果に埋め込まれ、誰でも読める
ブラウザに配信される時点で、すべて公開情報です。 DevTools の Sources タブで中身を読めます。
秘密鍵・API シークレットは、必ずサーバー側に置いてください(セキュリティ)。 フロントに置いてよいのは、公開されても困らない値だけです。
状態を4種類に分けて考える
フロントエンド設計で最も効く整理です。
| 種類 | 例 | どこに置くか |
|---|---|---|
| サーバー状態 | 商品一覧、ユーザー情報 | サーバーが正本。キャッシュとして扱う |
| クライアント状態 | モーダルの開閉、選択中のタブ | コンポーネントの state(React で画面を組み立てる) |
| URL の状態 | 検索条件、ページ番号、フィルタ | URL に持つ |
| フォームの状態 | 入力中の値、検証エラー | フォーム専用の仕組み |
// 悪い: 取得したデータを state に入れて、以降は自前で管理する
const [users, setUsers] = useState([]);
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []);この形は、必ず次の問題を生みます。
□ 他の画面で更新されても、こちらは古いまま
□ 再取得のタイミングを自分で管理することになる
□ ローディングとエラーの状態を毎回書く
□ 同じデータを複数の画面が別々に持つ
サーバーのデータは「自分が持っているもの」ではなく「借りているもの」です。 実務ではデータ取得ライブラリ(TanStack Query など)や、 フレームワークの仕組み(React で画面を組み立てるのサーバーコンポーネント)に任せます。
URL は状態である
悪い: 検索条件を state だけで持つ
→ リロードすると消える。共有できない。戻るボタンが効かない
良い: /search?q=keyword&page=2&sort=new
→ 共有できる。ブックマークできる。戻るが効く
「この画面をそのまま人に見せたいか」で判断してください。 検索結果・一覧のフィルタ・タブの選択は、たいてい URL に持つべきです。
画面には最低4つの状態がある
新人が最も見落とすところです。
1. 読み込み中 ローディング表示
2. 成功 データがある
3. 空 データが0件 ← 忘れられがち
4. エラー 取得に失敗した ← 忘れられがち
// データが無い時、何も表示されない画面ができあがる
{items.map((i) => <Row key={i.id} item={i} />)}利用者から見ると「壊れている」のと区別がつきません。
if (isLoading) return <Skeleton />;
if (error) return <ErrorState onRetry={refetch} />;
if (items.length === 0) return <EmptyState message="まだ登録がありません" />;
return items.map((i) => <Row key={i.id} item={i} />);空状態は、最初の利用者が必ず見る画面でもあります。 「ここから始めましょう」を置くと、そのまま案内になります。
API との連携
型を手で書かない
// 悪い: サーバーの変更に気づけない
type User = { id: string; name: string };
const res = await fetch('/api/users');
const users = await res.json() as User[]; // 実際の中身は保証されないas は「そういうことにする」だけで、検証していません(TypeScript の型)。
実務では次のどちらかを使います。
□ スキーマから型を生成する(OpenAPI / protobuf → 型定義)
→ サーバーが変わったら、ビルドで気づける
□ 実行時に検証する(zod など)
→ 想定と違うデータが来たら、その場で気づける
AI を製品に組み込むの「LLM の出力は外部入力」と同じで、 API のレスポンスも外部入力です。
失敗を前提にする
□ ネットワークエラー(オフライン、タイムアウト)
□ 4xx(入力が不正、権限が無い、期限切れ)
□ 5xx(サーバー障害)
□ 遅い(性能と負荷対策)
fetch は 404 でも例外にならない(JavaScript の基礎)ことを忘れないでください。
フォーム
バグが最も多く生まれる場所です。
□ 送信中はボタンを無効にする(二重送信の防止。見つけにくいバグ)
□ 検証はその場で出す(送信して初めてエラー、は最悪の体験)
□ エラーは項目のすぐそばに出す
□ 入力途中で消えないようにする(画面遷移・リロード)
□ 日本語入力の変換中に確定させない
HTML と CSSで書いたとおりですが、重要なので繰り返します。
HTML の required も、JS の検証も、迂回できます。
サーバー側の検証は必ず別に必要です。
フロントの検証は利用者を助けるため、 サーバーの検証はシステムを守るため。目的が違います。
認証まわりの実装
認証と認可の実装の内容が、フロント側ではこう現れます。
□ トークンをどこに置くか(localStorage は XSS で盗まれる)
□ 期限切れをどう扱うか(自動更新するか、ログイン画面に飛ばすか)
□ ログイン後、元のページに戻す(リダイレクト先を URL に持つ)
□ 未ログインで保護ページを開いた時の挙動
□ ログアウト時に、キャッシュしたデータを確実に捨てる ← 忘れられがち
最後の項目は事故になります。別のユーザーがログインした時に、 前のユーザーのデータが一瞬見えるというのは実際に起きます。
パフォーマンス
サーバー側(性能と負荷対策)と観点が違います。
| 指標 | 意味 | 主な原因 |
|---|---|---|
| LCP | 主要な内容が表示されるまで | 画像が大きい、サーバーが遅い |
| INP | 操作してから反応するまで | JS の実行が重い |
| CLS | 表示のガタつき | 画像の縦横指定が無い、後から要素が挿入される |
□ 画像は適切なサイズ・形式(WebP / AVIF)で、幅と高さを指定する
□ 最初に必要ないコードは遅延読み込みする
□ バンドルサイズを計測する(何が重いのかを見る)
□ 一覧が長いならページングか仮想スクロール(React で画面を組み立てる)
テスト
テストを書くのピラミッドは、フロントでもそのまま当てはまります。
| 種類 | 何をテストするか |
|---|---|
| 単体 | 純粋な関数(日付整形、金額計算、バリデーション) |
| コンポーネント | 「この props でこう表示され、押すとこう呼ばれる」 |
| E2E | 主要な業務フロー1本(ログイン → 検索 → 購入) |
□ 実装の詳細ではなく、利用者から見える振る舞いをテストする
□ 「クラス名が付いているか」ではなく「ボタンが押せるか」
□ E2E は少数に絞る(遅く、壊れやすい。テストを書く)
「ボタンが画面外にはみ出している」を自動で検出するのは困難です。
現実的な対策は、
□ 主要な画面幅(スマホ・タブレット・PC)で目視確認する手順を決める
□ 変更した画面のスクリーンショットを PR に貼る ← 効果が大きい
□ 必要ならビジュアルリグレッションテストを導入する
PR にスクリーンショットを貼るのは、コストが低く効果が高い習慣です。 レビュアーが実際に動かさなくても判断できます(ドキュメントを書く)。
デザイナーとの協業
□ デザインデータ(Figma など)が正本。目分量で作らない
□ 色・余白・文字サイズは**トークン**として定義し、直値を書かない
□ デザインに無い状態(エラー・空・長い文字列・読み込み中)は自分から確認する
□ 実装が難しい場合は「できません」ではなく、代替案と理由を出す(技術者の社会的責任)
デザインは、たいてい理想的なデータで描かれています。
□ 名前が非常に長い利用者がいたら?
□ 商品名が1文字だったら?
□ 一覧が0件だったら? 1000件だったら?
□ 画像が読み込めなかったら?
□ 通信が遅かったら?
これは見つけにくいバグの「境界値」と同じ発想です。 先に聞けば5分、後から直すと数日かかります。
ブラウザと端末の差
□ 対応する範囲をチームで決める(どのブラウザ・どの OS まで)
□ Safari は挙動が違うことがある(特に iOS)
□ スマホの実機で確認する(開発者ツールのエミュレートと差がある)
□ 100vh がモバイルのアドレスバーで期待通りにならない、など固有の罠がある
「自分の環境で動く」は、フロントエンドでは特に当てにならないと考えてください(開発環境を作る)。
商品一覧画面を実装します。API から取得したデータを表示するだけの単純な画面です。最低限、いくつの状態を実装しますか。
実務の落とし穴まとめ
- フロントの環境変数に秘密を入れる — すべて公開情報
- サーバー状態を state にコピー — 古くなり、同期の管理が必要になる
- 検索条件を URL に持たない — 共有できず、戻るボタンも効かない
- 0件とエラーの画面が無い — 真っ白になり、障害と区別できない
asで API のレスポンスを型付け — 検証していない- 送信ボタンを無効にしない — 二重送信
- フロントの検証を防御と考える — 迂回できる
- ログアウト時にキャッシュを捨てない — 前の利用者のデータが見える
- 開発機の速度で判断 — 実際の回線・端末で測る
- デザインに無い状態を確認しない — 長い文字列・0件・失敗時
- 実機で確認しない — 特に iOS Safari
まとめ
- ビルドの存在を知る。フロントの環境変数はすべて公開情報
- 状態はサーバー / クライアント / URL / フォームの4種類に分けて考える。 サーバーのデータは借りているもの
- URL は状態。共有・リロード・戻るが効くかで判断する
- 画面には読み込み中・成功・0件・エラーの最低4状態がある
- API のレスポンスは外部入力。型は生成するか、実行時に検証する
- フロントの検証は利用者のため、サーバーの検証はシステムのため
- パフォーマンスは LCP / INP / CLS。実際の環境で測る
- テストは利用者から見える振る舞いを。PR にスクリーンショットが効く
- デザインに無い状態(長い文字列・0件・失敗・遅延)を自分から確認する
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| web.dev(日本語) | https://web.dev/?hl=ja |
| MDN Web Docs(日本語) | https://developer.mozilla.org/ja/ |
| TanStack Query | https://tanstack.com/query/latest |
| Testing Library | https://testing-library.com/docs/ |
| WCAG 2.1(日本語訳) | https://waic.jp/translations/WCAG21/ |
章末問題
API のレスポンスを扱うコードで `const data = await res.json() as User[];` と書かれていました。何を指摘しますか。
次の章では、同じクライアント開発でも、 配ったら取り消せない世界——モバイルアプリを扱います。