はじめに
今回は、モダンなWeb開発のトレンドであるSSR(サーバーサイドレンダリング)について解説します。特に、Next.jsのようなフレームワークに頼らず、長年親しんできたLaravelとReact.jsを組み合わせてSSRを実現する方法を、具体的な構成案とともにご紹介します。なぜこの選択肢が有効なのか、そしてどうすれば実現できるのか、その詳細を見ていきましょう。
なぜNext.jsを使わずにSSRを検討するのか?
Next.jsは、ReactのSSR実装を簡単にする素晴らしいフレームワークです。しかし、既存のLaravelプロジェクトにSSRを導入したい場合、すべてをNext.jsに移行するのは現実的ではありません。また、Laravelで培ってきた豊富なバックエンドの知識やエコシステム(認証、Eloquent、Artisanコマンドなど)をそのまま活用したいケースも多いでしょう。
Next.jsを使わない独自のSSR構成を構築することで、以下のようなメリットがあります。
- 既存資産の活用: 既存のLaravelプロジェクトに段階的にSSRを導入できます。
- 柔軟なアーキテクチャ: フレームワークの制約に縛られず、独自の要件に合わせた構成を自由に設計できます。
- 技術スタックの統一: LaravelとReact.jsという、すでにチームが熟知している技術で一貫した開発が可能です。
Laravel開発者がSSRに挑戦するメリット
SSRを導入することで、Laravel開発者は以下のようなメリットを享受できます。
- SEOの改善: サーバー側でレンダリングされたHTMLは、クローラーが正しく内容を認識できるため、SPA(シングルページアプリケーション)の課題であるSEO(検索エンジン最適化)を改善できます。
- 初期表示の高速化: ユーザーはサーバーから完全なHTMLを受け取るため、JavaScriptのロードと実行を待つことなく、すぐにコンテンツを閲覧できます。
- UX(ユーザーエクスペリエンス)の向上: 初期表示が速くなることで、ユーザーの離脱率を下げ、より良いユーザー体験を提供できます。
本記事で構築するSSR構成の全体像
本記事では、Laravelがフロントコントローラーとして機能し、特定のリクエストに対して外部のNode.jsサーバーにReactコンポーネントのHTMLレンダリングを依頼する構成を構築します。このNode.jsサーバーが、Reactコンポーネントをサーバーサイドで実行し、生成されたHTMLをLaravelに返します。
SSRの基本とNext.jsとの違い
CSR(クライアントサイドレンダリング)とSSRの仕組み
CSR(クライアントサイドレンダリング): クライアント(ブラウザ)がサーバーからHTML、CSS、JavaScriptファイルを受け取った後、JavaScriptが実行されてDOM(Document Object Model)を生成・操作します。初期表示が遅くなる、SEOに弱いといった課題があります。
SSR(サーバーサイドレンダリング): サーバー側でHTMLを生成し、そのHTMLをクライアントに送ります。クライアントはHTMLを受け取ってすぐにコンテンツを表示できます。その後、クライアント側のJavaScriptが実行され、インタラクティブな機能が有効になります。
Next.jsが提供するSSR機能と、独自実装のメリット・デメリット
Next.jsは、ファイルシステムベースのルーティングや、getServerSidePropsといったAPIを提供することで、SSRをシンプルに実装できます。
Next.jsのメリット:
- 手軽にSSRが実現できる
- ルーティングやAPIなどが統合されている
- コミュニティが活発で、情報が豊富
独自実装のメリット:
- 既存のバックエンド資産を活かせる
- フレームワークに縛られない自由な設計が可能
独自実装のデメリット:
- 環境構築やルーティング、データフェッチなど、すべてを自分で設計する必要がある
- Next.jsが解決している多くの課題(コード分割、ハイドレーションなど)を自力で解決する必要がある
- LaravelでSSRを実現するための技術的課題
LaravelでSSRを実現するには、以下の技術的な課題を解決する必要があります。
- Node.jsサーバーの実行環境: PHPはJavaScriptを実行できないため、外部にNode.jsサーバーを立ち上げる必要があります。
- プロセス間通信: LaravelとNode.jsサーバー間で、リクエストデータやコンポーネント名などをやり取りする仕組みが必要です。
- ハイドレーション: サーバーでレンダリングされたHTMLと、クライアント側で実行されるReactのコンポーネントの状態を一致させる必要があります。
環境構築とコンポーネントの準備
Docker Composeによる開発環境のセットアップ
LaravelとNode.jsサーバー、そしてデータベースをまとめて管理するために、Docker Composeを使用します。
YAML
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: ./laravel
dockerfile: Dockerfile
volumes:
- ./laravel:/var/www/html
ports:
- "8000:8000"
depends_on:
- node-ssr
networks:
- srr-network
node-ssr:
build:
context: ./react-ssr
dockerfile: Dockerfile
volumes:
- ./react-ssr:/app
ports:
- "3001:3001"
networks:
- srr-network
nginx:
image: nginx:1.25.3-alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
networks:
- srr-network
networks:
srr-network:
driver: bridge
LaravelプロジェクトとNode.jsサーバーの連携
Laravel側からNode.jsサーバーにHTTPリクエストを送信するため、LaravelにHTTPクライアントを導入します。
Bash
docker-compose exec app composer require guzzlehttp/guzzle
Node.js側では、HTTPサーバーとしてExpressを使用し、Reactのレンダリングを行います。
react-ssr/server.js
JavaScript
// react-ssr/server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const app = express();
const path = require('path');
app.use(express.json());
app.post('/render', (req, res) => {
const { componentName, props } = req.body;
const Component = require(`./components/${componentName}`).default;
const element = React.createElement(Component, props);
const html = ReactDOMServer.renderToString(element);
res.json({ html });
});
app.listen(3001, () => {
console.log('SSR Server listening on port 3001');
});
SSR用のReactコンポーネント作成
サーバーとクライアントの両方でレンダリングされるように、Reactコンポーネントを作成します。ここではシンプルなAppコンポーネントを作成します。
react-ssr/components/App.jsx
JavaScript
// react-ssr/components/App.jsx
import React from 'react';
const App = ({ message }) => {
return (
<div>
<h1>{message}</h1>
<button onClick={() => alert('ボタンがクリックされました!')}>
クリック
</button>
</div>
);
};
export default App;
SSR実装のコアとなる部分
Node.jsサーバーでのReactコンポーネントのレンダリング
Laravelからリクエストを受け取ったNode.jsサーバーは、ReactDOMServer.renderToString()メソッドを使って、Reactコンポーネントを静的なHTML文字列に変換します。
JavaScript
const html = ReactDOMServer.renderToString(
React.createElement(Component, props)
);
Laravel側でのNode.jsサーバーへのリクエストとHTMLの受け取り
Laravel側では、HTTPクライアントを使ってNode.jsサーバーにPOSTリクエストを送信し、レンダリングされたHTMLを受け取ります。
PHP
// Laravelのコントローラー
use GuzzleHttp\Client;
public function index()
{
$client = new Client();
$response = $client->post('http://node-ssr:3001/render', [
'json' => [
'componentName' => 'App',
'props' => ['message' => 'Hello from SSR!']
]
]);
$html = json_decode($response->getBody()->getContents())->html;
return view('welcome', compact('html'));
}
この$html文字列を、LaravelのBladeテンプレートに埋め込んでユーザーに返します。
resources/views/welcome.blade.php
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Laravel SSR</title>
</head>
<body>
<div id="root">{!! $html !!}</div>
<script src="/js/bundle.js"></script>
</body>
</html>
{!! $html !!}という構文を使うことで、HTML文字列がエスケープされずにレンダリングされます。
ハイドレーション(Hydration)の仕組みと注意点
ハイドレーションとは、サーバーで生成された静的なHTMLに、クライアント側のJavaScriptを紐づけ、インタラクティブな機能(イベントハンドラーなど)を付与するプロセスです。
クライアント側のJavaScriptでは、ReactDOM.hydrate()メソッドを使います。
react-ssr/client.js
JavaScript
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './components/App';
const container = document.getElementById('root');
ReactDOM.hydrateRoot(
container,
<React.StrictMode>
<App message="Hello from SSR!" />
</React.StrictMode>
);
ハイドレーションを成功させるには、サーバーサイドとクライアントサイドでレンダリングされるコンポーネントツリーが完全に一致している必要があります。
データ取得と状態管理
サーバーサイドでのAPI呼び出しとデータのプリフェッチ
SSRでは、コンポーネントのレンダリング前に必要なデータをすべて取得しておく必要があります。これをデータプリフェッチといいます。
JavaScript
// react-ssr/server.js の拡張
// ...
app.post('/render', async (req, res) => {
const { componentName, props } = req.body;
const Component = require(`./components/${componentName}`).default;
// サーバーサイドでデータを取得
const data = await fetchData(props.postId);
const element = React.createElement(Component, { ...props, data });
const html = ReactDOMServer.renderToString(element);
res.json({ html });
});
コンポーネントの状態(State)をサーバーからクライアントへ引き継ぐ方法
サーバーで取得したデータをクライアント側でも利用できるように、HTMLに埋め込んで引き継ぎます。
HTML
<script>
window.__PRELOADED_STATE__ = {!! json_encode($data) !!};
</script>
<div id="root">{!! $html !!}</div>
クライアント側では、このwindow.__PRELOADED_STATE__からデータを取得して初期状態を復元します。
ReduxやZustandなどの状態管理ライブラリとの連携
状態管理ライブラリを導入する場合、サーバーとクライアントで同じストアを初期化する必要があります。
- サーバー: データを取得し、ストアを初期化して、ストアの状態をHTMLに埋め込む。
- クライアント: 埋め込まれた状態を読み込み、ストアを初期化する。
これにより、クライアント側のハイドレーションがスムーズに行われ、不必要な再レンダリングを防ぐことができます。
まとめとデプロイ
構築したSSR構成のレビュー
本記事で構築した構成は、以下の特徴を持っています。
- 疎結合: LaravelとNode.jsサーバーが分離されているため、それぞれを独立してスケールできます。
- 柔軟性: Next.jsのようなフレームワークの制約を受けず、要件に合わせて自由にカスタマイズできます。
- 既存技術の活用: Laravel開発者が培ってきたスキルと資産を最大限に活かせます。
実際のデプロイフローと注意点
デプロイ時は、LaravelアプリケーションとNode.jsサーバーをそれぞれ独立したプロセスとして起動し、両者が通信できるように設定する必要があります。
- Dockerコンテナの利用: Docker ComposeやKubernetesのようなコンテナオーケストレーションツールを使って、両方のサービスを管理するのが一般的です。
- 通信の最適化: サーバー間の通信は、ローカルネットワーク経由で行うことで、レイテンシを最小限に抑えます。
パフォーマンス最適化とキャッシュ戦略
SSRのパフォーマンスを最大化するためには、キャッシュ戦略が重要です。
- ページキャッシュ: サーバーでレンダリングされたHTMLをキャッシュすることで、同じリクエストが来たときに再レンダリングの負荷をなくせます。
- データキャッシュ: APIから取得したデータをキャッシュすることで、データベースへの負荷を減らします。
今回の記事では、LaravelとReact.jsを組み合わせ、あえてNext.jsに頼らない独自のSSR構成を構築する方法を解説しました。これが既存のLaravelプロジェクトにSSRを導入する際の選択肢となれば幸いです。
安心安全のホワイト高還元SESに転職を考えている方へ
新しい挑戦に踏み出すことは、人生において重要な一歩です。 転職活動は自分自身を知り、成長する貴重な機会でもあり、夢や成長を追求するためには必要な要素の一つ になるかと思います。 どんな選択をされるにせよ、その決断があなたに取って素晴らしい未来を切り開くことを願っています! グラディートと一緒に誇れるエンジニアを目指しましょう!
■『株式会社グラディート』では受託開発・SES・ブランディングデザイン・事業コンサルティングなどを事業として行う都内のIT企業です。現在、不遇な待遇で困っているエンジニアさんは、ぜひ一度グラディートに相談してみてね!(年収査定・SESへの転職相談も承っております!)
株式会社グラディート採用情報はこちら▼
https://en-gage.net/gradito/
株式会社グラディート公式サイトはこちら▼
https://www.gradito.co.jp/