🛣️

SPA?ルーティング?開発しながら学んでいく

2025/01/19に公開

はじめに

こんにちは、生成AIを活用しながら留学中の空いた時間を使って開発の勉強をしているニートです。
細かい自己紹介は初回の記事に書いてあります。

2025年始まってもう半月以上過ぎてしまいました。
日々英語と開発を過ごしていますが、時間が過ぎるのが速いです。

今回は React,Viteで作っている個人の自己紹介サイト(もともとは1画面だけで完結)にポートフォリオページ(サブページ)を追加する過程で、以下の部分が全然理解できていなかったことに気づけたので、学びをアウトプットしようと思います。

  • SPA(Single Page Application)って何?
  • ルーティングとは?
  • <a>タグと<Link>タグの違い
  • 静的ホスティングサービスの違い
  • BrowserRouterとHashRouter
  • Next.jsとReactは別物

実際のサイト=>自己紹介サイト(ポートフォリオ含む)

前提

今回Portfolioページを追加するサイトは、もともと1画面のみの作りに各種外部サービスへのリンクがついているだけでした。
React, Vite, Typescript, TailwindCSSで作られており、create.xyzかbolt.newでポンっと出たものを多少編集したぐらいだったと思います。
なので内部的な作りは、
index.htmlがあり、<script>タグで main.tsx を呼び出し
main.tsxApp.tsx と流れてApp.tsxが1画面のほとんどを構成要素を記述していました。
(SNSアイコンなどの小コンポーネントは別で切り出していたが、ほとんどデザインがApp.tsxに集約されていた、という意味です。)

今回の開発

1画面だったサイトに内部リンクをつけて、/portfolio のページを追加したい!
というのが今回の開発の出発点です。
方針は単純で、App.tsx でルーティングの設定をして、元々App.tsxに記述していたメインページコンテンツをpagesディレクトリ配下のHome.tsx に移行、プラスでPortfolio.tsx を作成する。というものでした。
しかし、これが意外と上手くいかないんですね。
実際にアクセスするとブラウザで404エラーが出てしまいます。

このエラーを解消するためには、最初に上げた今回の学習項目を理解しておく必要がありました。

1. SPA(Single Page Application)って何?

このエラーの根底をなしているのが、Reactは基本的に SPA として動作するということです。
ではSPAとは何でしょうか?SPAは 1枚のHTMLとJSだけで多画面のように見せる仕組み になります。

今回の場合で言うと、
ルートにindex.htmlというエントリーポイントのhtmlファイルが1つあるだけで、
それ以外に /portfolio.htmlファイル や /portfolio/ディレクトリ配下のindex.htmlファイルがあるわけではありません。

サイトにアクセスした際は、必ずこのエントリーポイントのindex.htmlに入る必要があります。
他のページへの割り振りをするためにはルーティング設定をはじめとするjavascriptの処理が必要になります。
SAPを利用すると、ページの切り替えがブラウザ上のjsで制御されるため、クライアント側で完結しブラウザのリロードを伴わない高速なページ切り替えが可能になるなどのメリットがあります。
一方で、エントリーポイントのindex.htmlを経由せずにサーバーに直接/portfolioでアクセスする場合や、内部リンクでアクセスしたのちにリロードして再度サーバーにリクエストを投げる場合などは、そこ(/portfolio)にhtmlファイルは存在しないため、404エラーが発生してしまいます。

2. ルーティングとは?

ルーティングは、URLとページ表示(コンポーネント切り替え)を結びつける仕組み です。
今回の改修の中では、まずApp.tsxは以下のように設定しました。

App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import Portfolio from './pages/Portfolio';

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/portfolio" element={<Portfolio />} />
            </Routes>
        </BrowserRouter>
    );
}

これによって、メインのページ(/)ではHomeComponentを表示、/portfolioページではPortfolioComponentを表示するように指定しています。

3. <a>タグと<Link>タグの違い

もう一点、クライアントサイドでページの切り替えをするために押さえておく必要があるのが、通常のhtmlで利用される<a>タグと、ReactComponentである<Link>タグの違いです。

  • <a href="/portfolio">: ブラウザのデフォルト挙動でページ遷移し、リロードが走る
  • <Link to="/portfolio">: React Routerがクライアントサイドでパスを切り替え、リロードしない

SPAのjsで制御されるページ切り替えは、クライアントで完結してブラウザのリロードは発生しません。
つまり、ブラウザのデフォルト挙動でページ遷移とリロードが走る<a>タグとは相性が悪いのです。
Reactを利用したSPAでは、内部のページ切り替えは<Link>タグを利用する必要があります。

4. 静的ホスティングサービスの違い

残った2つの問題に対応するためには、どうすれば良いのでしょうか?
1つは、 サーバーにアクセスリクエストがあった時に、エントリポイントのルートのindex.htmlにリダイレクトする というものです。
しかし、これはサーバ側の制御になるため、利用しているホスティングサービスによって対応方針が異なります。

無料でホスティングできる以下の3つのサービスで対応方針を記載します。

  • Netlify: _redirectsファイルで設定
/*    /index.html   200
  • Vercel: vercel.jsonでRewrite設定できる。Next.jsとの親和性が高い
vercel.json
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}
  • GitHub Pages: サーバー側での対応は不可、404.htmlを活用してリダイレクトさせることは可能

私は今回のサイトのホスティングには github pages を利用していました。(VercelとNetlisyは他の開発で利用しているためです)
現状ホスティングサービスは意図的に散らして利用しているので、ホスティングサービスを変更することはしませんでした。
また、404pageからリダイレクトさせるのもちょっとしっくり来ず、この対応は見送りました。

5. BrowserRouterとHashRouter

ではこの問題は解決できないのでしょうか?
「直接URLで指定して /portfolio にアクセスする場合」には対応できませんが、
「内部リンクでアクセスした後にリロードする場合」には対応する方法があります。
それが HashRouter の利用です。

ReactRouterにはBrowserRouterとHashRouterがあります。
HashRouterはURLに/#/が含まれる形でルーティングします。つまり、 /portfolio だったのが /#/portfolio のような形式になります。
#以降はサーバーに送られない仕組みなっているようで、 /#/portfolio にアクセスしても必ずエントリポイントの index.html が適切に返されるため404エラーを回避することができます。

App.tsx
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import Portfolio from './pages/Portfolio';
import Layout from './components/layout/Layout';

function App() {
  return (
    <Router>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/portfolio" element={<Portfolio />} />
        </Routes>
    </Router>
  );
}

export default App;

したがって、今回の私のサイトでいうと以下のURLに直接アクセスすると挙動が異なります。

HashRouterのデメリットとしては、URLに # が含まれて違和感がある ということと、#以降はサーバーに送られないため SEOにも弱い みたいです。

BrowserRouterについては詳細は省略しますが、HTML5のHistory API(pushStateやreplaceState)を使用してURLを管理する仕組みを取っており、クリーンなURL(例: /portfolio, /products/123)を提供します。

6. Next.jsとReactは別物

これは小噺ですが、Next.jsはReactをベースにしたフレームワーク と言われていますよね。
言っていることは全く嘘ではないと思うのですが、私はこの表現から、Next.jsとReactの作りは似ているだろう と思っていました。
しかし、これは大きな間違えを引き起こす可能性があるので気をつけてください。
少なくとも複数ページのサイトを作成して、Next.jsで特徴的なSSRやSSGを利用する場合、ReactのSAPとは全く異なる構成になります。

おわりに

自分の中でやりたいことがあればそれを実現するために新たな学びが生まれるので、今回の試みは非常に良かったです。
portfolioページの今後の改修に関してはissueにも書いているのですが、技術スタックのタグの利用などはoperationを踏まえると直したいところはまだまだあるので、継続的に直していきたいと思っています。

最後に話は少しそれますが、生成AIの進化がとどまることを知らないですね。
初回の記事でソフトウェア開発生成AIとして以下のモノを紹介していました。
create.xyz, v0 by vercel, Bolt.new, GPT engineer etc..
v0やboltはまだ話に聞きますが、最近はAI agentとして、Devin, cline, replit agent, cursor composerなどの方がよく聞くようになりました。私もclineを入れて使っています。エディタから離れずに開発が進むというのは素晴らしいですよね。

一方で、開発の過程でAIとやり取りしながらその場をしのげても、自分の頭がついていってないような感覚にも陥ります。気になったところは質問すれば、かなり適切な回答が返ってくるようになりましたし、学習の速度が飛躍的に伸びたことも疑いようがないですが、私的にはそれを読んでわかった気になるのではなく、自分の言葉でアウトプットまでして、自分の頭の中で整理された知識として持つところまでもっていくことが重要だと思っています。

今回の知識があれば会社のHPをSPAのReactで作ろうとは思わないですよね。サーバーサイドで処理すれば対応できる部分もあるでしょうが、SEO対応しつつ、どのパスに直接アクセスされてもちゃんと表示される方が要件としては重要になるのであればMPAの方が適していそうです。
知識がなくても、『何となく』のものは作れてしまうのが生成AIの恐ろしさですよね。今はそれなりの見た目のものが誰でも気軽に作れてしまうので、実際のビジネス要件に対応できるレベルの知識があるかどうかの判断は難しくなりますよね。。。発注者側もある程度の知識がないと適切な委託者を見抜けなくなってしまうリスクは認識しておいた方がよさそうです。

以上です。ありがとうございました。

Discussion