🏗️

Laravel (Blade) モノリス環境でのフロントエンド技術選定と設計

に公開

背景と課題

弊社プロダクトは Laravel ベースのモノリシック構造を基盤に複数のサービスを展開しており、既存プロダクトは Vue.js(Atomic Design を採用)+ TypeScript + Laravel (Blade) + Bootstrap4 で実装された MPA 構成でした。

しかし以下の課題がありました。

  • ページ遷移ごとにフルリロードが発生し、UX が損なわれる
  • Vue.js の環境下に Bootstrap4 + jQuery が混在し、保守性が低下

これらを解決すべく、新規プロダクトでは Vue.js を採用せず、
代わりに Laravel (Blade) + React (SPA) + TypeScript + Tailwind CSS を採用しました。

技術刷新

React を選択した理由

  • 2025年現在のフロントエンドの主流であり、採用メリットが大きい
  • Storybook, Testing Library, Vitest などエコシステムが豊富で保守しやすい
  • 既存の Laravel モノリス環境との整合性を重視したため、Next.js は採用しなかった

Next.js を見送った理由は以下です。

  • プロダクトがBtoB向けの認証前提の業務システムのため、SEOやパブリックページ高速化といった Next.js の強みを活かす場面が少ないと判断
  • Next.js のファイルルーティングや機能は魅力的だが、既存プロダクトとの整合性を優先
  • 学習コストやベンダーロックインのリスクも考慮した

Tailwind CSS と shadcn/ui

  • デザインの統一と効率化を重視し、Tailwind CSS を採用
  • コンポーネントは shadcn/ui を導入し、共通 UI を再利用可能に

アーキテクチャ改善

  • 弊社におけるAtomic Design 運用の課題
    • molecule / organism などの分類が曖昧になりやすく、チームでの認識がぶれる
    • ディレクトリ階層が深くなり、どこにコンポーネントを置くべきか迷うケースが多発
    • 結果としてコンポーネント責務が不明瞭になり、再利用性やテストのしやすさを損ねた
    • ロジックと UI の境界線が不明確なため、テスト導入が困難になり、品質保証が属人的になった

新プロダクトでは features ディレクトリ構成を採用し、責務分離を明確にしました。

  • Presentation 層: 見た目とインタラクションに特化
  • Container 層: データ取得やロジック統合
  • Hooks (useXxx.ts): 再利用可能なビジネスロジック

まず、プロジェクト全体のディレクトリ構成を整理しました(抜粋)。

assets/
├── js/
│   ├── components/
│   ├── constants/
│   ├── contexts/
│   ├── api/
│   ├── features/
│   ├── hooks/   # プロジェクト全体で共通利用するカスタムフック
│   ├── libs/
│   ├── utils/
│   ├── routes/
│   └── app.tsx
└── views/   # Laravel エントリーポイント (index.blade.php)

  • components/: プロジェクト全体で共通利用する UI コンポーネント
  • constants/: 定数をまとめた場所。アプリ全体で使う固定値を定義
  • contexts/: useContextを定義。グローバルで扱う認証情報やユーザー設定などを保持
  • features/: 機能ごとのディレクトリ
  • hooks/: 共通カスタムフック
  • libs/: ライブラリ設定
  • utils/: ピュア関数・ヘルパ
  • routes/: React Router によるルーティング定義
  • api/: TanStack Query による API メソッドを実装

features ディレクトリ例: Dashboard

features/
 └── Dashboard/
      ├── components/
      │   └── DashboardWidget/
      │        ├── useDashboardWidget.ts
      │        ├── DashboardWidgetContainer.tsx
      │        ├── DashboardWidgetPresenter.tsx
      │        └── DashboardWidgetPresenter.stories.ts
      ├── useDashboard.ts
      ├── DashboardContainer.tsx
      ├── DashboardPresenter.tsx
      └── DashboardPresenter.stories.ts
  • Presenter: 見た目専用
  • Container: ロジック統合
  • useXxx.ts: ビジネスロジック
  • .stories.ts: Storybook 用の UI テスト / ドキュメント
  • useXxx.tsContainer.tsx はロジックがない場合、省略可能

コードの例

// useUserList.ts (ビジネスロジック)
import { useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/api/users";

export const useUserList = () => {
  const { data, isLoading } = useQuery(["users"], fetchUsers);
  return { data, isLoading };
};
// UserListContainer.tsx (ロジック統合)
import { useUserList } from "./useUserList";
import { UserListPresenter } from "./UserListPresenter";

export const UserListContainer = () => {
  const { data, isLoading } = useUserList();
  return <UserListPresenter data={data} isLoading={isLoading} />;
};
// UserListPresenter.tsx (見た目専用)
type Props = {
  data: { id: number; name: string }[] | undefined;
  isLoading: boolean;
};

export const UserListPresenter = ({ data, isLoading }: Props) => {
  if (isLoading) return <p>Loading...</p>;
  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

このように分離することで、UI・ロジック・データ取得を明確に切り分けられ、
Storybook やテストの記述もシンプルになります。

責務分離のルール

ファイル種別 役割 やっていいこと やってはダメなこと
xxxPresenter.tsx 見た目専用 props の表示 / UI インタラクション / レイアウト 状態管理 / API 呼び出し
xxxContainer.tsx ロジック統合 Hooks の呼び出し / props の受け渡し DOM 操作 / スタイリング
useXxx.ts ビジネスロジック 状態管理 / API 呼び出し / 計算処理 JSX return / UI 固有処理

実際には きれいに分離できないグレーゾーンも多く存在します。

例えば useState を本当に Container 側で持つべきか迷うケースがありますし、
react-hook-form の配置場所も常に悩ましい問題です。

そのため、ケースによっては features/components/ 配下の子コンポーネントでも
Containerを導入して親から注入するcomposition的なことをして、実践的に悩みながら工夫しています。

Storybook とテスト導入

  • Storybookにより UI コンポーネントのカタログ化とレビュー効率化
  • play関数を使い、クリックや入力などのインタラクションをブラウザ上で確認
  • VitestでHooksやユニットレベルのロジックテストを実施
  • test runner + GitHub Actions CI で PR ごとに UI テストを自動実行

Container / Presentation / Hooks パターンを採用したことで、
Storybook やテストの記述が格段にやりやすくなりました。

従来は…

  • 依存関係のあるAPI モックの設定が大変
  • 子コンポーネントの動作を制御するために多くの準備コードが必要
  • Storybook でレイアウトだけ確認したいのに、ビジネスロジックや依存関係を揃える必要がある

といった問題がありました。

Presentation 層を「見た目専用」に分離したことで、こうした準備コストを大幅に削減できました。

  • Presenter は props を渡すだけで Story を作成できる
  • Container は Hooks と結合して動作確認をしたいときにのみ利用
  • Hooks は単体で Vitest によるロジックテストを書ける

この分離により、「UI の確認」と「ビジネスロジックの検証」を切り離せる ようになり、チーム全体の開発効率が向上しました。

おわりに

今回は新規プロダクトということもあり、Reactのアーキテクチャ部分から検討させていただきました。この経験から、技術選定は流行りだけでなく、既存プロダクトとの整合性を意識することが重要だと学びました。
完璧な理想形の実現は難しいものの、日々改善を続けていくことの大切さを実感しています。
今後は Storybook をデプロイし、Chromatic の導入や既存プロダクトへの横展開も視野に入れていきたいと考えています。

Booost

Discussion