プログラマのための IT 教科書

フロントエンドの実務

この部の 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 にコピーして持たない
// 悪い: 取得したデータを 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. エラー      取得に失敗した    ← 忘れられがち
「0件」と「エラー」を作らないと、本番で真っ白になる
// データが無い時、何も表示されない画面ができあがる
{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 で画面を組み立てる)
まず「本当に遅いのか」を測る

計算量とデータ構造・性能と負荷対策と同じで、推測で最適化しないでください。

□ DevTools の Lighthouse / Performance タブ
□ 実際の利用者の環境で測る(開発機は速すぎる)
□ 低速回線・低速端末をエミュレートして確認する

開発者の Mac と光回線で速いことは、何の保証にもなりません。

テスト

テストを書くのピラミッドは、フロントでもそのまま当てはまります。

種類何をテストするか
単体純粋な関数(日付整形、金額計算、バリデーション)
コンポーネント「この props でこう表示され、押すとこう呼ばれる」
E2E主要な業務フロー1本(ログイン → 検索 → 購入)
□ 実装の詳細ではなく、利用者から見える振る舞いをテストする
□ 「クラス名が付いているか」ではなく「ボタンが押せるか」
□ E2E は少数に絞る(遅く、壊れやすい。テストを書く)
見た目の崩れは、テストで自動検出しにくい

「ボタンが画面外にはみ出している」を自動で検出するのは困難です。

現実的な対策は、

□ 主要な画面幅(スマホ・タブレット・PC)で目視確認する手順を決める
□ 変更した画面のスクリーンショットを PR に貼る  ← 効果が大きい
□ 必要ならビジュアルリグレッションテストを導入する

PR にスクリーンショットを貼るのは、コストが低く効果が高い習慣です。 レビュアーが実際に動かさなくても判断できます(ドキュメントを書く)。

デザイナーとの協業

□ デザインデータ(Figma など)が正本。目分量で作らない
□ 色・余白・文字サイズは**トークン**として定義し、直値を書かない
□ デザインに無い状態(エラー・空・長い文字列・読み込み中)は自分から確認する
□ 実装が難しい場合は「できません」ではなく、代替案と理由を出す(技術者の社会的責任)
デザインに無いものを見つけるのが実装者の仕事

デザインは、たいてい理想的なデータで描かれています。

□ 名前が非常に長い利用者がいたら?
□ 商品名が1文字だったら?
□ 一覧が0件だったら? 1000件だったら?
□ 画像が読み込めなかったら?
□ 通信が遅かったら?

これは見つけにくいバグの「境界値」と同じ発想です。 先に聞けば5分、後から直すと数日かかります。

ブラウザと端末の差

□ 対応する範囲をチームで決める(どのブラウザ・どの OS まで)
□ Safari は挙動が違うことがある(特に iOS)
□ スマホの実機で確認する(開発者ツールのエミュレートと差がある)
□ 100vh がモバイルのアドレスバーで期待通りにならない、など固有の罠がある

「自分の環境で動く」は、フロントエンドでは特に当てにならないと考えてください(開発環境を作る)。

商品一覧画面を実装します。API から取得したデータを表示するだけの単純な画面です。最低限、いくつの状態を実装しますか。

実務の落とし穴まとめ

  1. フロントの環境変数に秘密を入れる — すべて公開情報
  2. サーバー状態を state にコピー — 古くなり、同期の管理が必要になる
  3. 検索条件を URL に持たない — 共有できず、戻るボタンも効かない
  4. 0件とエラーの画面が無い — 真っ白になり、障害と区別できない
  5. as で API のレスポンスを型付け — 検証していない
  6. 送信ボタンを無効にしない — 二重送信
  7. フロントの検証を防御と考える — 迂回できる
  8. ログアウト時にキャッシュを捨てない — 前の利用者のデータが見える
  9. 開発機の速度で判断 — 実際の回線・端末で測る
  10. デザインに無い状態を確認しない — 長い文字列・0件・失敗時
  11. 実機で確認しない — 特に 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 Queryhttps://tanstack.com/query/latest
Testing Libraryhttps://testing-library.com/docs/
WCAG 2.1(日本語訳)https://waic.jp/translations/WCAG21/

章末問題

API のレスポンスを扱うコードで `const data = await res.json() as User[];` と書かれていました。何を指摘しますか。

次の章では、同じクライアント開発でも、 配ったら取り消せない世界——モバイルアプリを扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。