Laravelオンリーのプロダクトに、inertia.jsをつかって段階的にReactを導入・移行する方法を確立してみました!

はじめに

これは、我々エンジニアグループがInertia.jsでBladeを殺さずReactを飼い慣らし、 次の7年を戦う足場を築いた記録である。

こんにちは!ウィルゲート開発グループの中尾(@NobleNomad41)です。

最近、弊社のWebプロダクトで「LaravelオンリーからReactへの段階的移行」を確立・実践してみました。正直なところ、最初は「本当にうまくいくんだろうか?」と半信半疑でしたが、実際にやってみると予想以上にうまくいきませんでした。やっぱりだめじゃないか。でも成功しました。

備忘録も兼ねて、LaravelオンリーからフロントエンドへReact導入の段階的移行手法を後世に残そうと思います。

振り返ってみると、Node.js大幅アップデートでの互換性問題、shadcn/ui導入でのWebpack格闘、設計思想の試行錯誤など、予想以上に多くの困難に直面しました。ただ、これらの経験を通じて得られた知見は、同じような課題を抱えている方にとって価値があるのではないかと考えています。

実際に手を動かして分かった段階的移行の方法論を、失敗談も含めて詳しく共有したいと思います。

TL;DR

  • Blade(+jQuery)を崩すことなく、1ページごとにInertia.js+Reactへ移行する手段や試した設計
  • Inertia.js依存を薄め"いつでもSPA/API分離OK"にする疎結合アーキテクチャの実例
  • Node.js 10→18/Webpack 4→5アップグレード地獄を突破した具体的ハマり&解決策

現状の課題とフロントエンド刷新の必要性

我々のプロダクトは7年間、Laravel Blade + jQueryの構成で開発されてきました。当時の開発メンバーがその時点での最適解として選択した技術スタックで、事業要件を満たすための現実的な判断だったと思います。

ただ、機能が増えるにつれて以下のような課題が顕在化してきました。

フロントエンドの設計指針が明確でない状態で開発が続いており、機能追加のたびに複雑化していました。技術負債も蓄積されており、Node.js 10.15という古いバージョンを使用していたため、セキュリティ面での懸念もありました。

開発効率の面でも、新機能開発に想定以上の時間がかかったり、修正による予期せぬ副作用でデバッグに時間を取られることが増えていました。また、フロントエンド起因の問題により、UI/UXの課題による操作ミスや表示崩れで業務に支障が出ることもありました。

これらの課題を踏まえ、フロントエンド技術の刷新を決断しました。

LaravelからReactへの段階的移行戦略

移行戦略の全体像

フロントエンド刷新を検討する際、以下の4つの戦略を比較検討しました。

戦略 アプローチ メリット デメリット 工数 採用判断
JavaScript設計導入 既存jQueryに設計指針を追加 低リスク、短期間 根本的な解決にならない
Vue.js刷新 Laravel + Vue.jsで全面刷新 モダンな開発体験 学習コスト、移行工数大
SPA完全移行 React + API化で完全分離 最新技術フル活用 開発コスト膨大、リスク高 特大
段階的移行 Inertia.js + React段階導入 既存資産活用、低リスク 移行期間の技術混在

事業継続性とリスク管理を重視した結果、Inertia.js + React段階的移行を選択しました。

なぜInertia.js + Reactだったのか

4段階の移行計画

以下の4段階での移行計画を策定しました。

フェーズ 内容 期間 主な成果物
Phase 1 Inertia.js導入基盤構築 1ヶ月 Node.js環境刷新、基盤環境構築
Phase 2 React + UIライブラリ導入 2週間 UIコンポーネント基盤構築
Phase 3 プロダクションコード段階的移行 継続中 ページ単位での移行、ノウハウ蓄積
Phase 4 完全分離への準備(将来) 未定 API化・SPA移行検討

なぜInertia.jsを選択したのか

Inertia.jsを選択した主な理由は以下の通りです。

まず、既存Laravel資産の活用が可能でした。ルーティング、認証、バリデーションをそのまま利用でき、従来通りのController → Bladeレンダリングも継続できます。同時に、必要に応じてAPIエンドポイントの作成も可能で、この柔軟性がInertia.jsの大きな魅力です。

学習コストも低く、既存のLaravel知識を活かしながらSPAライクな体験を実現できます。また、段階的移行への適合性が高く、ページ単位での移行が可能で、既存ページとの共存も容易でした。

Phase 1: Inertia.js導入とReact基盤構築

Inertia.js環境構築

Laravel側ではinertiajs/inertia-laravelパッケージをインストールし、HandleInertiaRequestsミドルウェアを設定しました。Node.js側では@inertiajs/reactと関連パッケージを導入しました。

<?php
// HandleInertiaRequestsミドルウェアの設定例
public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'auth' => [
            'user' => $request->user(),
        ],
        'flash' => [
            'message' => fn () => $request->session()->get('message')
        ],
    ]);
}

今回の環境では、Laravel 8 + Webpackでの構成としました。当プロダクトはもともとLaravel 8でしたが、これだとViteを導入できなかったため、泣く泣くWebpackを使って導入しました。理想を言うとViteの方が開発体験は良いのですが、現実的な制約の中での最適解としてWebpackを選択しました。

Phase 2: React + UIライブラリ導入

React 19および周辺ライブラリの導入

React 19への移行では、いくつかの破壊的変更に対応する必要がありました。特にreact-dom/clientへの移行と@inertiajs/reactへの更新が主要な変更点でした。

UIライブラリの選定と導入

統一感のあるUIを効率的に構築するため、TailwindCSSとshadcn/uiの組み合わせを選択しました。

shadcn/uiは、TailwindCSSで構築されたコンポーネント群であり、Headless UIライブラリ(@radix-ui)をベースにしています。これにより、アクセシビリティとカスタマイズ性のバランスが良好なUIコンポーネントを活用できました。

// UIコンポーネントの活用例
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Dialog } from '@/components/ui/dialog';

const FormComponent = () => {
    return (
        <Card className="p-6">
            <Dialog>
                {/* アクセシブルでカスタマイズ可能なコンポーネント */}
            </Dialog>
        </Card>
    );
};

設計思想の確立

Inertia.js非依存の疎結合設計

将来的なSPA移行を見据えて、Inertia.js固有の機能への依存を最小限に抑制する設計方針を採用しました。

// ❌ 悪い例(Inertia.js直接依存)
const FormComponent = () => {
    const { data, setData, post } = useForm(); // Inertia.js固有
    // ...
};

// ✅ 良い例(疎結合設計)
const FormComponent = () => {
    const { data, updateData, submitForm } = useCustomForm();
    // Inertia.jsの詳細はカスタムフック内に隠蔽
    // ...
};

具体的には、Inertia.js固有の処理を専用のカスタムフックに分離し、将来的にInertia.jsの使用をやめても移行しやすい構造としました。

ディレクトリ構成での関心の分離

react/
├── app.jsx                    # エントリーポイント
├── components/                # 汎用コンポーネント
│   ├── ui/                   # 最小単位UIコンポーネント(shadcn/ui)
│   └── layouts/              # 共通レイアウトコンポーネント
├── features/                 # 機能固有コンポーネント
│   └── [domain]/
│       ├── hooks/           # カスタムフック
│       ├── utils/           # Pure JSロジック
│       └── components/      # UIコンポーネント
└── pages/                   # ページコンポーネント

重要なのは、hooks/utils/の使い分けです。hooksはReactに依存するロジック、utilsはReactに依存しないPure JavaScriptロジックを配置することで、テスタビリティと再利用性を向上させようと試みました。

ただし、改修前の既存コードには設計上の課題がありました。これは当プロダクト固有の問題だと思いますが、本来Presentation層で変換されるべき値が適切に処理されておらず、PHPのクラスやModelを直接Bladeテンプレート内で読み込み、複雑な判定や加工処理を行っている箇所が多数存在していました。

React移行時にはこれらの複雑なビジネスルールをフロントエンド側で再実装する必要が生じ、当初はutilsフォルダ配下にまとめる方針で進めました。しかし結果として、ファイルが膨れ上がったり、単一のファイルが複数の責務を持ってしまったりといった事態に陥りました。これは明確な反省ポイントです。

この経験から、複雑なビジネスルールを持つページには、より高度なドメイン駆動設計やFeature Sliced Designなど、ドメインロジックを適切に扱えるアーキテクチャの採用を検討する良い機会となりました。

Phase 3: 段階的ページ移行の実践

移行対象ページの選定

最初の移行対象として、機能的に独立性が高く、影響範囲を最小限に抑えられる管理画面の一部を選択しました。この画面を選んだ理由は、ユーザー数が限定的で、問題が発生した場合の影響を最小限に抑えられるためです。

移行前後の変化

移行前は、HTMLとJavaScriptが混在し、バリデーションロジックも分散していました。移行後は、ロジックとUIが明確に分離され、型安全性も向上しました。

特に印象的だったのは、画面の見た目の統一感が向上したことです。shadcn/uiによる一貫したデザインシステムにより、ユーザビリティが大幅に改善されました。

バリデーション処理の再設計

既存のLaravelバリデーションロジックをそのまま活用することはできなかったため、React側で新たに実装しました。重要なポイントは、Inertia.js依存部分を分離し、バリデーションロジック自体はPureなCustom Hookとして実装したことです。

// バリデーションロジックの実装例
export const useFormValidation = () => {
    const validateForm = useCallback((data: FormData) => {
        const errors: Record<string, string> = {};

        if (!data.name || data.name.length < 3) {
            errors.name = '名前は3文字以上で入力してください';
        }

        return errors;
    }, []);

    return { validateForm };
};

// Inertia.js依存部分の分離
export const useFormWithInertia = () => {
    const { validateForm } = useFormValidation();

    const submitForm = async (data: FormData) => {
        const errors = validateForm(data);
        if (Object.keys(errors).length > 0) {
            return { errors };
        }

        // Inertia.jsの呼び出しはここに隠蔽
        await router.post('/api/endpoint', data);
    };

    return { submitForm };
};

Controller側の対応とAPI化

移行対象ページでは、Controllerを以下のように変更しました。

表示系の変更

既存のControllerを、従来のBladeビューの代わりにInertia::renderを使用するように変更しました。

<?php
// Controller変更例(表示系)
public function edit($id)
{
    $item = Model::findOrFail($id);

    // React版を返す
    return Inertia::render('Pages/ItemEdit', [
        'item' => $item,
        'options' => $this->getFormOptions(),
    ]);
}

保存・更新処理のAPI化

UX向上を図るため、保存・更新処理など一部のControllerはAPIに変更しました。これにより、ページリロードなしでのSPA的な動作を実現し、ユーザー体験が大幅に向上しました。

<?php
// API化したController例
public function store(Request $request)
{
    $validated = $request->validate($this->validationRules());

    $item = Model::create($validated);

    // JSON形式でレスポンス(SPA的な動作)
    return response()->json([
        'message' => '保存が完了しました',
        'item' => $item
    ]);
}

この組み合わせにより、Inertia.jsの利便性とAPI化によるUX向上の両方を実現できました。読者の方にも「Inertia.jsとReactありじゃん!!」と感じていただけるのではないでしょうか。

実装で得られた知見

開発体験は明らかに向上しました。型安全性により開発時のエラー検出が早くなり、統一されたエラーハンドリングでコードの可読性も大幅に向上しました。

また、UIコンポーネントの再利用性が高まったことで、新しい画面を作成する際の工数も削減されました。shadcn/uiによる一貫したデザインシステムにより、画面間の操作感の統一も実現できました。

現在は一人で移行作業を進めている段階ですが、実装パターンのドキュメント化を進めており、将来的にはチーム内での知識共有と学習を通じて、チーム全体でのReact開発体制を構築していく予定です。

具体的には、ペアプログラミングを積極的に活用して知識移転を進めたり、社内勉強会を定期的に開催してチーム全体のスキル向上を図ったりすることを考えています。また、今回の移行で得られた実装パターンをドキュメント化して、チームメンバーが参考にできる資料として整備していく計画です。

このように個人の取り組みからスタートして、段階的にチーム全体の技術力向上へと発展させていく方針で進めています。

Phase 4: 将来の展望

現在検討中の次のステップとして、全ページの移行が完了したら、完全にフロントエンドをLaravelから分離することを考えています。

API化への準備

段階的API化を進める予定です。Inertia.jsとAPIリクエストを共存させる形で、将来のSPA移行に備えた設計を検討しています。

<?php
// Inertia.jsとAPIの共存パターン
public function index(Request $request)
{
    $items = Model::all();

    // APIリクエストかInertia.jsリクエストかで分岐
    if ($request->wantsJson()) {
        return response()->json($items);
    }

    return Inertia::render('Pages/ItemIndex', compact('items'));
}

設計思想の進化

今回の経験を踏まえて、次回の改修では以下の方針を検討しています。

単純なCRUD画面は現在のfeatureディレクトリ構成で十分ですが、複雑なドメインロジックを持つ画面や、複数のエンティティが絡む画面、将来的な機能拡張が予想される画面には、Feature Sliced Designなどより高度な設計パターンの適用を検討したいと考えています。

技術的困難と解決策

移行過程では、いくつかの技術的困難に直面しました。以下、苦労し、試みた解決策を共有します。

Node.js大幅アップデートでの格闘

Node.js 10.15から18.xへの大幅アップデートでは、予想以上に多くの互換性問題に遭遇しました。

脳筋戦法で、動くまでひたすら必要なライブラリアップデートや代替などを繰り返しました。特にOpenSSL互換性問題では、--openssl-legacy-providerオプションの追加が必要でした。

また、node-sassがNode.js 18に対応していなかったため、Dart Sassへの移行も必要でした。Webpack 4から5への移行では、設定ファイルとビルドスクリプトの全面的な更新が求められました。

さらに、CI/CD環境でのECS image更新も必要でしたが、これは3年ぶりの更新となり、考慮もれが発生しやすいポイントでした。

TypeScriptパスエイリアスの導入

Laravel 8 + Webpack環境でTypeScriptのパスエイリアスを使用するため、tsconfig-paths-webpack-pluginを導入しました。これにより、相対パスの複雑化を避けることができました。

全網羅テストによる品質担保

Node.js環境の大幅アップデートを行ったため、既存のフロントエンド画面がすべて正常に表示・動作することを確認する全網羅テストを実施しました。品質担保のためには必須の作業でした。

実践を通じた学び

技術選定の重要性

理想的な技術スタックと現実的な制約のバランスを取ることの重要性を実感しました。最新技術の採用も重要ですが、現在の環境でできる最適解を見つけることも同じく重要です。

段階的移行の価値

一気に全てを変更するのではなく、段階的に移行することで、リスクを最小限に抑えながら技術革新を進められました。既存資産を活かしつつ現代的な開発体験を獲得できたのは大きな成果です。

設計思想の重要性

将来の変更を見据えた疎結合設計と適切なアーキテクチャパターンの選択が、長期的な保守性向上に大きく寄与することを実感しました。

一人で始める移行戦略

チーム全体での学習に至っていない現段階でも、一人で移行を始めて知見を蓄積し、将来のチーム化に備えるという戦略は有効でした。

まとめ

LaravelオンリーのプロダクトからReactへの段階的移行を実践して、以下の重要なポイントを学びました。

段階的移行の有効性は確実です。リスクを最小限に抑えながら技術革新が可能で、既存資産を活かしつつ現代的な開発体験を獲得できました。

Inertia.jsを活用した移行戦略は、Laravel資産を活用しながらReactを導入する現実的なアプローチです。特に、疎結合設計により将来のSPA移行への道筋も確保できました。

技術選定では、理想と現実のバランスが重要です。最新技術への憧れだけでなく、現在の制約下での最適解を見つけることが成功の鍵でした。

同じような課題を抱える方には、段階的アプローチを強くおすすめします。一気に変更するのではなく、段階的に移行することで、リスクを分散しながら継続的な改善を進められます。

今回の移行プロジェクトは、技術的な改善だけでなく、現実的な問題解決能力の向上にもつながりました。数字による定量的な改善を追求しつつ、美しい設計への執着も大切にしながら、継続的な改善を進めていきたいと思います。

同じようにLaravelからReact移行を検討されている方、もしくは既に挑戦された方がいらっしゃいましたら、ぜひコメントで体験談をお聞かせください!特に『こんな罠にハマった』『こうするともっと良かったよ』みたいな話は大歓迎です。


使用技術

Laravel 8、React 19、TypeScript、Inertia.js、Tailwind CSS、shadcn/ui、Webpack