モダンなWebフロントエンド開発で押さえておきたい原則と実践をまとめたガイドです。フレームワーク(React / Vue / Svelte など)に依存しない内容を中心に、必要に応じて具体例を添えています。
目次
- 設計・アーキテクチャ
- コンポーネント設計
- 状態管理
- TypeScript と型安全
- HTML とセマンティクス
- CSS / スタイリング
- アクセシビリティ(a11y)
- パフォーマンス
- データ取得と非同期処理
- エラーハンドリング
- セキュリティ
- テスト
- コード品質とツール
- ビルド・デプロイ・運用
- チェックリスト
1. 設計・アーキテクチャ
-
機能(feature)単位でディレクトリを切る 種類別(
components/,hooks/,utils/)だけで分けると、機能が増えるほど関連ファイルが散らばります。機能ごとにまとめ、共有部品だけをshared/に置く構成が保守しやすいです。src/ ├── features/ │ ├── auth/ │ │ ├── components/ │ │ ├── hooks/ │ │ ├── api.ts │ │ └── index.ts # 公開APIを限定する │ └── cart/ ├── shared/ │ ├── ui/ # Button, Modal など汎用UI │ └── lib/ └── app/ # ルーティング・エントリポイント -
関心の分離:UI(表示)、ロジック(状態・副作用)、データアクセス(API)を分ける。
-
依存の方向を一方向に:
features → sharedは可、shared → featuresは不可。循環依存を作らない。 -
レンダリング戦略を意識的に選ぶ:CSR / SSR / SSG / ISR / Server Components の特性(初期表示速度、SEO、サーバー負荷、動的性)を理解し、ページごとに適切なものを選ぶ。
-
YAGNI:将来の拡張を見越した過剰な抽象化は避け、必要になってから共通化する(「3回出てきたら抽象化」が目安)。
2. コンポーネント設計
-
単一責任:1つのコンポーネントは1つの役割に。200〜300行を超えたら分割を検討。
-
Props は最小限かつ明示的に:何でも受け取る巨大な Props オブジェクトより、意図が明確な少数の Props を。
-
合成(Composition)を優先:Props で分岐を増やすより
childrenやスロットで差し替え可能にする。// ✗ フラグで分岐が増え続ける <Card showHeader showFooter headerTitle="..." footerAction={...} /> // ○ 合成で柔軟に <Card> <Card.Header>タイトル</Card.Header> <Card.Body>...</Card.Body> </Card> -
プレゼンテーションとコンテナを意識:見た目だけの部品は副作用を持たせず、データ取得は上位やカスタムフックに寄せる。
-
制御 / 非制御を明確に:フォーム部品などは
value+onChange(制御)かdefaultValue(非制御)かを統一。 -
key には安定した一意IDを使う:配列のインデックスを key にすると並び替え時にバグの原因になる。
-
副作用は最小限に:
useEffectは「外部システムとの同期」のためのもの。派生値の計算やイベント処理に使わない。
3. 状態管理
-
状態は必要な場所のできるだけ近くに置く(コロケーション)。最初からグローバルにしない。
-
状態の種類を区別する
種類 例 推奨手段 サーバー状態 APIから取得したデータ TanStack Query, SWR, RTK Query など UIローカル状態 モーダルの開閉、入力値 コンポーネント内の state グローバルUI状態 テーマ、ログインユーザー Context, Zustand, Pinia など URL状態 検索条件、ページ番号、タブ クエリパラメータ / ルートパラメータ フォーム状態 入力・バリデーション React Hook Form, VeeValidate など -
派生できる値は state にしない:
fullNameはfirstNameとlastNameから計算する。 -
サーバー状態を手書きの global store に複製しない:キャッシュ・再取得・無効化は専用ライブラリに任せる。
-
共有・ブックマークしたい状態は URL に持たせる。
-
イミュータブルに更新する:オブジェクトや配列は直接書き換えず新しい値を作る。
4. TypeScript と型安全
-
strict: trueを有効にする(noUncheckedIndexedAccessも推奨)。 -
anyを避け、不明な値はunknownで受けて絞り込む。 -
外部入力は実行時に検証する:APIレスポンスやフォーム入力は Zod / Valibot などでスキーマ検証し、型を推論させる。
const User = z.object({ id: z.string(), name: z.string() }); type User = z.infer<typeof User>; const user = User.parse(await res.json()); -
判別可能なユニオンで状態を表現する:「ありえない状態」を型で表現できなくする。
type State = | { status: "loading" } | { status: "success"; data: User[] } | { status: "error"; error: Error }; -
型アサーション(
as)は最終手段。使う場合は理由をコメントに残す。 -
API の型は可能なら自動生成(OpenAPI, GraphQL Codegen など)してフロント・バックの乖離を防ぐ。
5. HTML とセマンティクス
- 意味に合った要素を使う:
<button>,<a>,<nav>,<main>,<header>,<article>,<section>,<ul>など。 - クリック可能なものは
<button>か<a>:<div onClick>はキーボード操作・スクリーンリーダーに対応しない。- ページ遷移 →
<a href> - アクション実行 →
<button type="button">
- ページ遷移 →
- 見出しレベルを飛ばさない(
h1→h3にしない)。ページにh1は基本1つ。 - フォーム要素には必ず
<label>を関連付ける。 <img>にはaltを:装飾画像はalt=""。lang属性と適切な<meta>(viewport, description, OGP)を設定する。
6. CSS / スタイリング
-
スタイリング手法をチーム内で1つに統一(CSS Modules / Tailwind CSS / CSS-in-JS(ゼロランタイム系推奨)など)。
-
デザイントークンを使う:色・余白・フォントサイズ・角丸などは CSS カスタムプロパティやトークンで一元管理し、マジックナンバーを避ける。
:root { --color-primary: oklch(55% 0.2 260); --space-2: 0.5rem; --radius-md: 8px; } -
レイアウトは Flexbox / Grid。
floatや絶対配置の多用を避ける。 -
モバイルファーストで書き、
min-widthのメディアクエリで拡張する。コンポーネント単位ならコンテナクエリも有効。 -
相対単位を使う:フォントは
rem、ユーザーの文字サイズ設定を尊重する。 -
詳細度を低く保つ:ID セレクタや
!importantを避ける。:where()やカスケードレイヤー(@layer)で管理。 -
ユーザー設定を尊重する:
prefers-color-scheme,prefers-reduced-motion,prefers-contrast。 -
アニメーションは
transformとopacity中心に(レイアウト再計算を避ける)。
7. アクセシビリティ(a11y)
アクセシビリティは「追加機能」ではなく品質の一部です。WCAG 2.2 AA 準拠を目標にしましょう。
- キーボードだけで全操作できること。Tab 順序が論理的であること。
- フォーカスを見えるように:
outline: noneで消さない。:focus-visibleで調整する。 - コントラスト比:通常テキスト 4.5:1 以上、大きいテキスト 3:1 以上。
- 色だけで情報を伝えない:エラーは色+アイコン+テキストで。
- ARIA は最後の手段:「ネイティブ要素で表現できるなら ARIA を使わない」が第一原則。
- 動的な変化を通知:トーストやエラーは
aria-live領域で読み上げさせる。 - モーダルはフォーカストラップと Esc での閉じる操作を実装し、閉じたら元の位置にフォーカスを戻す(
<dialog>要素の活用も検討)。 - タッチターゲットは十分な大きさに(目安 24×24px 以上、推奨 44×44px)。
- 自動テスト(axe など)+手動テスト(キーボード操作、スクリーンリーダー)を組み合わせる。自動ツールで検出できるのは一部の問題のみ。
8. パフォーマンス
指標を測る
- Core Web Vitals を追う:
- LCP(最大コンテンツの描画):2.5 秒以内
- INP(操作への応答性):200ms 以内
- CLS(レイアウトのずれ):0.1 以下
- ラボデータ(Lighthouse)とフィールドデータ(実ユーザー計測 / RUM)の両方を見る。
読み込みを軽くする
- バンドルサイズを監視し、予算(パフォーマンスバジェット)を決める。
- コード分割:ルート単位・重いコンポーネント単位で遅延読み込み(
import())。 - 不要な依存を入れない:小さな用途に巨大ライブラリを使わない。tree-shaking が効く書き方を。
- 画像最適化:
- AVIF / WebP を使う、
srcset/sizesでサイズを出し分ける width/heightを指定して CLS を防ぐ- ファーストビュー外は
loading="lazy"、LCP 画像はfetchpriority="high"
- AVIF / WebP を使う、
- フォント:サブセット化、
font-display: swap、必要なものだけpreload。 - 重要リソースは
preconnect/preload、サードパーティスクリプトはdefer/asyncや遅延読み込みで。 - キャッシュ:ハッシュ付きファイル名+長期キャッシュ、HTML は短期キャッシュ。
実行を速くする
- メインスレッドを長時間ブロックしない:重い処理は分割(
scheduler.yield()/requestIdleCallback)または Web Worker へ。 - 不要な再レンダリングを避ける:まずは状態の置き場所を見直す。メモ化(
memo/useMemo/useCallback)は計測して効果があるところに(React Compiler 利用時は自動化される)。 - 長いリストは仮想化(TanStack Virtual など)。
- 入力に連動する処理はデバウンス / スロットル。
9. データ取得と非同期処理
- ローディング・エラー・空状態を必ず設計する(成功時だけ作らない)。
- ウォーターフォールを避ける:独立したリクエストは並列化(
Promise.all)、可能ならルート単位で先読み。 - キャッシュと再検証:Stale-While-Revalidate 戦略をライブラリで活用。
- 競合状態に注意:古いリクエストの結果で新しい結果を上書きしない(
AbortControllerでキャンセル)。 - 楽観的更新はUXを向上させるが、失敗時のロールバックを必ず実装する。
- APIクライアントを一箇所に集約:認証ヘッダ、エラー変換、リトライ方針を共通化。
- スケルトンUI でレイアウトシフトを抑えつつ待ち時間を短く感じさせる。
10. エラーハンドリング
- エラーバウンダリで画面全体のクラッシュを防ぎ、部分的にフォールバックUIを表示。
- ユーザー向けメッセージは具体的かつ次の行動が分かるように(「エラーが発生しました」だけにしない)。
- エラーを握りつぶさない:
catchして何もしないコードを書かない。 - 監視ツールで本番エラーを収集(Sentry など)。ソースマップをアップロードしてスタックトレースを読めるように。
- フォームのバリデーションはクライアント側(UX向上)とサーバー側(信頼性確保)の両方で行う。
11. セキュリティ
- XSS 対策:
- フレームワークの自動エスケープに任せ、
innerHTML/dangerouslySetInnerHTML/v-htmlを避ける - どうしても HTML を挿入する場合は DOMPurify などでサニタイズ
hrefにjavascript:スキームが入らないよう URL を検証
- フレームワークの自動エスケープに任せ、
- Content Security Policy(CSP) を設定する。
- 機密情報をフロントに置かない:APIキーやシークレットはバンドルに含めればすべて公開される。
- トークン保管:可能なら
HttpOnly/Secure/SameSite付き Cookie を使い、localStorageへの保存は避ける。 - CSRF 対策:SameSite Cookie、CSRF トークン。
- 依存パッケージの脆弱性を定期チェック(
npm audit, Dependabot, Renovate)。lockfile をコミットする。 - 外部リンク:
target="_blank"にはrel="noopener noreferrer"。 - クライアントの認可チェックは表示制御にすぎない。本当の認可は必ずサーバーで。
12. テスト
テストのバランス(Testing Trophy の考え方):静的解析 → 単体 → 結合(最重視)→ E2E。
| 層 | 対象 | ツール例 |
|---|---|---|
| 静的解析 | 型・構文・規約 | TypeScript, ESLint |
| 単体 | 純粋関数、ユーティリティ | Vitest, Jest |
| 結合 / コンポーネント | ユーザー操作に対する振る舞い | Testing Library, Vitest Browser Mode |
| E2E | 重要なユーザーフロー | Playwright, Cypress |
| ビジュアル回帰 | 見た目の差分 | Storybook + Chromatic, Playwright スクリーンショット |
-
実装詳細ではなく振る舞いをテストする:ユーザーが見る・操作するもの(ロール、ラベル、テキスト)でクエリする。
// ○ ユーザー視点 await user.click(screen.getByRole("button", { name: "送信" })); expect(screen.getByText("送信しました")).toBeVisible(); -
APIはネットワーク層でモック(MSW など)して、現実に近い条件でテスト。
-
E2E はクリティカルパスに絞る(ログイン、購入など)。数を増やしすぎると遅く不安定になる。
-
不安定(flaky)なテストを放置しない。
-
Storybook などで UI カタログを作り、状態ごとの見た目をドキュメント化する。
13. コード品質とツール
- Lint / Format を自動化:ESLint + Prettier、または Biome など。保存時と CI の両方で実行。
- pre-commit フック(lefthook, husky + lint-staged)で問題のあるコードがコミットされるのを防ぐ。
- 命名は意図を表す:
data,info,handleClick2ではなく、userProfile,handleSubmitOrder。 - コメントは「なぜ」を書く(「何を」はコードで表現する)。
- 小さく頻繁にプルリクエストを出す。レビューしやすい単位で。
- 依存関係を定期的に更新(Renovate / Dependabot で自動化)。
- ドキュメント:README にセットアップ手順、主要な設計判断は ADR(Architecture Decision Record)で記録。
14. ビルド・デプロイ・運用
- CI で必ず実行:型チェック → Lint → テスト → ビルド。
- プレビュー環境:プルリクエストごとにデプロイしてレビューに使う。
- 環境変数を型付きで管理し、ビルド時に欠落を検出する。公開してよい変数とそうでない変数を区別する。
- フィーチャーフラグで段階的リリースや即時ロールバックを可能にする。
- 監視:エラー監視、RUM(Core Web Vitals)、必要に応じてプロダクト分析。
- ロールバック手順を用意しておく。
15. チェックリスト
リリース前に確認したい項目です。
機能・UX
- ローディング / エラー / 空状態が実装されている
- モバイル・タブレット・デスクトップで表示崩れがない
- 主要ブラウザで動作確認済み
- フォームのバリデーションとエラーメッセージが分かりやすい
アクセシビリティ
- キーボードだけで全操作ができる
- フォーカスが視認できる
- 画像に適切な
altがある - コントラスト比が基準を満たしている
- 自動チェック(axe 等)で重大な問題がない
パフォーマンス
- LCP / INP / CLS が目標値内
- 画像が最適化され、サイズ指定がある
- バンドルサイズが予算内
- 不要なサードパーティスクリプトがない
セキュリティ
- シークレットがクライアントに含まれていない
- 未サニタイズの HTML 挿入がない
- 依存パッケージに既知の重大な脆弱性がない
- CSP などのセキュリティヘッダが設定されている
品質
- 型エラー・Lint エラーがない
- 重要なフローがテストでカバーされている
- エラー監視が有効になっている
参考リソース
- MDN Web Docs
- web.dev(Core Web Vitals、パフォーマンス)
- WCAG 2.2 / WAI-ARIA Authoring Practices Guide
- OWASP Cheat Sheet Series
- Testing Library — Guiding Principles