Laravel(API)とReact(Next.js+TypeScript)で作るwebサービス構成

2023年の今、とても今更な構成ですが自分なりに色々まとめながら開発環境構築。ちなみにMacです。

※Next.jsはSSRではなくSPAとして使うパターンを想定しています。それならNext.jsじゃなくていいのでは?と思いますよね。実際そうだと思います!笑

PHP & Laravelの準備

PHPは実行環境を整える作業と、Composer等のツールを使える様にする作業が存在します。
今回、実行環境はMAMPを選定。ダウンロードするだけなのでとても簡単。こちらの過去記事を参考にしていただくと、debug環境まで準備できるのでおすすめです。

LaravelのプロジェクトをMAMPのドキュメントディレクトリ内に作成することで、デバッグもできてMySQLも使えてしまうわけですね。

では準備ができたら今回用のサンプルプロジェクトを作成しておきましょう。

composer create-project --prefer-dist laravel/laravel SampleProject

Reactの準備

まずは以下のコマンドでインストール&バージョン確認。

brew install nodejs

node -v
npm -v

次にreactプロジェクトを作成するのですが、2023年現在、create react appの利用は非推奨とされています。公式的には「今時なんらかのフレームワーク使うでしょ?」というスタンスみたいです。

私が今回フレームワークの候補として選定したのは以下の2パターン。

1.vite + React
2.Next.js

どちらも勢いがあってメリットがあるのですが、今回はNext.jsを使うことにしました。

Next.jsでプロジェクトを作成する際には任意のディレクトリにて以下のコマンドを実行。
ちなみに自分はTypeScriptで作成します。

npx create-next-app nextsample

//- TypeScriptの場合
npx create-next-app nextsample --typescript

末尾は好きなプロジェクト名に変更してください。

以下のようにパッケージのインストールが必要と聞かれるので「y」を入力。

Need to install the following packages:
  create-next-app@13.4.7
Ok to proceed? (y) 

以下聞かれるので選択。[]の中は私の選択です。

✔ Would you like to use ESLint with this project? … No / Yes [Yes]
✔ Would you like to use Tailwind CSS with this project? … No / Yes [No]
✔ Would you like to use `src/` directory with this project? … No / Yes [Yes]
✔ Use App Router (recommended)? … No / Yes [Yes]
✔ Would you like to customize the default import alias? … No / Yes [Yes]
✔ What import alias would you like configured? … [@/*]

TailWind以外は「Yes」にしています。また下の2行を設定することで、相対パスではなくルートからの絶対パスが指定できる様になるとのこと。今回でいえば@/hoge/huga/…のように記述することが可能みたいです。

この状態でサーバを立ち上げてみましょう。このコマンドはVSCodeを使っているのならターミナルで実行すると、視線をずらさずとも状態が確認できるのでおすすめです。

npm run dev

localhost:3000にアクセスすると表示されるはずです。

Laravel側サンプルコーディング

今回Laravel側はプライベートAPIサーバとして動作させるイメージです。まずはコントローラを作成しましょう。

php artisan make:controller TestController --api

作成後、上のファイルを開き、indexを編集。

    public function index()
    {
        return response()->json(['status' => 'ok']);
    }

これをルーティングに登録します。routesの中にあるapi.phpを開いてください。
※Laravel8以降はルーティングへの記述方法が変わった様です。古い記述方法だと以下の様なエラーが発生します。

Target class [**Controller] does not exist.

正しい記述方法はこちら。

//useを追加
use App\Http\Controllers\TestController;

~~~

Route::get('/test', [TestController::class, 'index']);

これでURLにアクセスするとシンプルなJsonが返ってくるはずです。
URLは私の環境では「http://localhost:8888/LaravelSample/public/api/test」でした。

React側コーディング

今回はLaravelとnode.jsのポートが違うため、擬似的に別ドメイン(別オリジン)環境であることを再現できています。

また、最終的にはCSRにて動かすことを想定しています。

ページ追加とApp routerについて

Next.jsはバージョン13からルーティングに関する考え方が変わっています。基本的には
1.URLにしたいディレクトリ名
2.その中にpage.tsx
を作成する必要があります。もし/testという階層のページを作成したい場合は、
Appの中にtestというディレクトリを作成し、その中にpage.tsxを作成します。

export default function Page() {
  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      test
    </main>
  )
}

page.tsxには上記を記述して保存してください。この状態でlocalhost:3000/testにアクセスするとグレーの背景にtestというテキストが表示されるだけのシンプルな画面が表示されます。

この画面に押下するとリクエストを投げるボタンを追加してみましょう。
まずは先ほどのコードにTestButtonを追加します。

export default function Page() {
  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      test
      <TestButton />
    </main>
  )
}

const TestButton = () => {
  return <button className='test-button'>GET</button>
}

reactの特徴である、関数型コンポーネントという仕組みでボタンを作成しています。このようなDOMの再利用性は生産性を上げてくれますね。

次にTestButtonに加えたclass名test-buttonに対してcssを記述します。Next.js 13系であればappフォルダの中にglobals.cssが存在するので以下を加えてください。

.test-button{
  background-color: rgb(81, 125, 174);
  color: white;
  padding: 0.8rem;
  border-radius: 0.5rem;
  cursor: pointer;
}

globals.cssは共通で読み込まれるcssです。
この状態でブラウザを確認すると、ボタンぽいボタンが表示されます。

今度はこのボタンに動きをつけましょう。
test/page.tsxを以下のように変更します。

'use client';

let result: string = 'none';

export default function Page() {
  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      <p id="result-field">{result}</p>
      <TestButton />
    </main>
  )
}

const TestButton = () => {
  return <button className='test-button' onClick={ GetRequest } data-url='http://localhost:8888/LaravelSample/public/api/test'>GET</button>
}

async function GetRequest(e: React.MouseEvent) {
  const targetURL = e.currentTarget.getAttribute('data-url');
  if(targetURL == null || targetURL == '')
    alert('URLError!');
  else{
    await fetch(targetURL, {
        method: 'GET'
      }).then(
          response => {
            if (response.status != 200) {
                  alert('エラー');
              } else {
                  return response.json();
              }
          }
      )
      .then((jsonData) => {
          if (jsonData != null) {
              const status = jsonData['status'];
              if ( status == 'ok') {
                  result = status;
              } else {
                result = 'ステータスエラー';
              }
          } else {
              result = 'レスポンスエラー';
          }
      })
      .catch(error => {
          result = 'catch!';
      });
    let resultField = document.getElementById('result-field');
    if(resultField)
      resultField.innerText = result;
  }
}

順に説明すると、まずTestButtonに対してonClickで実行される関数を登録しています。data-url=〜という記述が追加されていますが、HTML5よりdata-*とした部分はgetAttributeで取得することが可能です。これで擬似的に引数の様な使い方をしています。(これが最適解かはさておき。。。)

GetRequest内ではURLが正しくセットされていた場合に実行する処理を記述しています。この辺りはReactがどうこうではないのでピンとこない方はAxiosで検索してください。最終的に結果を表示用pタグテキストにしています。

okが出れば成功です!

終わりに

個人的に思いましたがNext.jsは13から始めるとわかりにくいかも知れません。笑
ただ今までに他のフレームワーク等の経験がある方や昨今のフロントのトレンドを理解していれば(SSR,SSG..)、今までのバージョンと変わったところがどこなのかピンときやすいのかなと思います。

自分も最近はサーバサイドやアプリ開発ばかりやっていたので、なかなか思ったものと違ったなという感じです。。定期的なキャッチアップ大事ですね。