この記事について
Reactの開発ではcreate-react-appを用いることが多かったのですが、Next.jsでも開発を行う機会があったので把握できた範囲で実装方法の差異などをまとめてみたいと思います。
個人的な内容整理が主目的ですが、どちらかに触れてみようと思っている方やフレームワーク選定の際の参考になれば幸いです。
SPA、SSR、SSGについておさらい
フレームワークの話の前にこの辺りの用語についておさらいしておきます。
SPAの登場前は、
ページ遷移の度にサーバーへ同期処理でリクエストを行い、ページを取得するのが一般的でした。
LaravelやRailsなどMVCフレームワークで画面作ったことある方はイメージしやすいと思います。
ページアクセスの度にサーバーでPHPやRubyなどの処理が実行されるため、サーバーの負荷や読み込みの遅さなどが課題とされていました。
SPAの登場
ページ遷移の度に同期で読み込みするの煩わしい!ってことで生まれたのがSPA(シングルページアプリケーション)です。SSR(サーバーサイドレンダリング)と比較してCSR(クライアントサイドレンダリング)とも呼ばれたりもします。
サーバーへ同期でリクエストするのはサイトアクセス時の1回のみで、このタイミングでjsファイル等必要なものを全てブラウザへダウンロードします。以降はブラウザ上の操作(ページ遷移やボタン押下など)によって非同期でサーバーへリクエストを行い、DOMを書き換えることによってあたかも複数ページあるような振る舞いをします。
しかし、SPAは以下が弱点と言われています。
-
初回読み込みの遅さ
・・・初回アクセス時に必要なファイルを全てダウンロードするので、読み込みに時間かかる。← これよく言われるやつですが、初回だけなのでそんなにデメリットではないかも、、個人的にそこまで気になる印象は今の所ないです。 -
ページごとにOGPが設定できない
・・・OGPは、Open Graph Protocolの略で、Twitterとかにリンクを貼るとリンク先の概要や画像を表示してくれる仕組みのことです。本来ページごとにOGPの設定ができるようですが、SPAの実態は1ページだけなので、1つのページのOGPしか設定できません。 -
SEOが弱い
・・・検索エンジンクローラーでは動的に切り替えられたDOM読み込みができないため、検索上位にヒットさせることが難しい。※
※ 補足:今はクローラーが進化してきており、動的変更でも読み込みできるようになってきていると言われています。とはいえクローラーが見た時点では(サーバーに配置されている状態では)SPAは要素が少ないのでSEOが弱いと言われているのではないかと思います。
SSRの登場
SPAの弱点を克服するために生まれたのがSSR(サーバーサイドレンダリング)です。SPAとの違いはアクセスごとにサーバーでページを作成して返却する点です。
これにより初回読み込みで必要なファイルを全てダウンロードする必要がなくなるため、初回読み込みの遅さは解決します。また、アクセス時(クローラーが見た時も含む)にサーバーでページが作成されるので、OGPやSEOの問題も解決できます。
非同期でサーバーへリクエストする点はSPAと変わらないので、最初に解説した毎回サーバーに同期でリクエストを出す方式ではありません。
しかし、サーバーサイドで処理する点は同じなので、Node.jsの準備や負荷を考える必要が出てきます。
SSGの登場
SSRのサーバー保守の観点をなくすために出てきたのが、SSG(静的サイトジェネレート)です。
SSGではビルド時にAPIを叩くなどして必要な情報を取得し、ページを作成するような実装が可能です。
予め全ページを作成してサーバーにファイルを配置するので、SSRのメリットは活かせつつNode.jsの準備が不要になります。(SPAと同じようにNginxやApacheなどのWebサーバーがあればOK。)
また、ページ表示の速度もSPAやSSRに比べ高速になります。
SSGは静的サイトジェネレーターと呼ばれることが多いと思いますが、これだとフレームワークやCMSと横並びにするのが正しいと思います。
以降、この記事内でのSSGはNext.jsの「ビルド時生成」の意味で記載します。
では全部SSGでよいのか?
ここまでの内容をまとめるとSSGが最強に思えてきますが、例えば作りたいものが「商品が頻繁に追加になるECサイト」の場合を考えてみます。
ビルド時にAPIを叩いて商品情報を取得し、ページを作成するような実装の場合、商品が追加になるごとにビルドが必要になります。
商品が追加になる度に自動でビルドが走るようなCI/CDの仕組みを構築すればいいだけですが、それならそもそもSSRで実装しても良いような気もします。
とは言え少し前までSSR用フレームワークと言われていたNext.jsもSSG対応が可能になり、SSG対応を推奨している所を見ると世の中的にもSSGを推す傾向にあるのかなと。
https://nextjs.org/docs/basic-features/pages#pre-rendering
長くなりましたが今回のフレームワークは、
- create-react-appはReactの
SPA
用フレームワーク - Next.jsはReactの
SSR
・SSG
用フレームワーク
これを踏まえて本題へ。
ここから下に出てくるサンプルコードはtypescriptになります。
Reduxの部分はReduxToolKitを使用しています。
また、Next.jsは9以降を想定した内容になります。
1. プロジェクト作成手順
まずはプロジェクトの作り方の違いについて。
どちらもコマンドで簡単にプロジェクト作成が可能で、様々なテンプレートが用意されています。
create-react-appの場合
create-react-app
コマンドを使用します。
テンプレートについては以下の通り。
https://create-react-app.dev/docs/custom-templates/
npmでcra-template-*
と検索することで様々なテンプレートを見つけることができます。
create-react-app {プロジェクト名} --template redux-typescript
Next.jsの場合
create-next-app
コマンドを使用します。
公式から色んなテンプレートが公開されています。
https://github.com/vercel/next.js/tree/master/examples
create-next-app --example with-typescript {プロジェクト名}
2. ページ遷移の方法について
次に実装に関する内容です。
リンクやボタンでページ遷移(画面切り替え)を行う時の方法について。
create-react-appの場合
こちらはreact-router-dom
を使用するのがメジャーな方法かと思います。
まず、待ち構えている側(ルーティング定義)の例です。
以降、このMainApp.tsxはindex.tsx(エントリポイント)から呼び出されるものと仮定します。
TopContainer
、HogeContainer
、FugaContainer
の中でそれぞれUI描画処理が記述(componentの呼び出しが)されているイメージです。
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import TopContainer from './containers/TopContainer';
import HogeContainer from './containers/HogeContainer';
import FugaContainer from './containers/FugaContainer';
const MainApp: React.FC = () => (
<Router>
<Switch>
<Route exact path="/" component={TopContainer} />
<Route path="/hoge" component={HogeContainer} />
<Route path="/fuga" component={FugaContainer} />
</Switch>
</Router>
);
export default MainApp;
次にページ遷移する側の例です。
import React from 'react';
import { Link } from 'react-router-dom';
const SampleApp: React.FC = () => (
<div>
<Link to="/">topへ遷移</Link>
<Link to="/hoge">hogeへ遷移</Link>
<Link to="/fuga">fugaへ遷移</Link>
</div>
);
export default SampleApp;
Next.jsの場合
一方こちらはNext.jsの標準で提供されているnext/link
を使用します。こちらはreact-router-domと同様クライアントサイドでの遷移になります。
先ほど紹介したcreate-next-appでプロジェクト作成するとpages
というディレクトリができます。
このpages
ディレクトリ配下にリンク名と同じ名前でファイルを作成しておくことでnext/link
による遷移が可能になります。
/(ルート)はindexという名前にします。
import React from 'react';
import TopContainer from './containers/TopContainer';
const index: React.FC = () => (
<div>
これはtopページです
<TopContainer />
</div>
);
export default index;
hogeページ用
import React from 'react';
import HogeContainer from './containers/HogeContainer';
const hoge: React.FC = () => (
<div>
これはhogeページです
<HogeContainer />
</div>
);
export default hoge;
fugaページ用
import React from 'react';
import FugaContainer from './containers/FugaContainer';
const fuga: React.FC = () => (
<div>
これはfugaページです
<FugaContainer />
</div>
);
export default fuga;
遷移する側は、ライブラリが異なるだけで使用感に大差はありません。
import React from 'react';
import Link from 'next/link';
const SampleApp: React.FC = () => (
<div>
<Link href="/top">topへ遷移</Link>
<Link href="/hoge">hogeへ遷移</Link>
<Link href="/fuga">fugaへ遷移</Link>
</div>
);
export default SampleApp;
3. 共通レイアウトの適用方法
全ページに共通のレイアウトを適用させたい時の方法も異なります。
共通レイアウトはこんな感じのものを使用するとします。
import React from 'react';
import { Link } from 'react-router-dom';
const Layout: React.FC = ({ children }) => (
<div>
<div>ヘッダー</div>
<div>
{children}
</div>
<div>フッター</div>
</div>
);
export default Layout;
create-react-appの場合
create-react-appは先ほど登場したMainApp.tsxに手を加えることで簡単に共通のレイアウトを設定できます。
ページによって切り替えたい部分をLayoutタグで囲ってあげるだけでOKです。
Layoutコンポーネントのchildren
の部分が、Layoutタグで囲われた内側の要素になるため、どのページに遷移しても共通のレイアウト(Layoutコンポーネント)が適用されます。
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Layout from './Layout';
import TopContainer from './containers/TopContainer';
import HogeContainer from './containers/HogeContainer';
import FugaContainer from './containers/FugaContainer';
const MainApp: React.FC = () => (
<Router>
<Switch>
<Layout>
<Route exact path="/" component={TopContainer} />
<Route path="/hoge" component={HogeContainer} />
<Route path="/fuga" component={FugaContainer} />
</Layout>
</Switch>
</Router>
);
export default MainApp;
Next.jsの場合
こちらはカスタムアプリという機能を使用します。
カスタムアプリについて公式ドキュメント
https://nextjs.org/docs/advanced-features/custom-app
https://nextjs.org/docs/basic-features/typescript#custom-app ← typescript版はこっち
pages
ディレクトリ配下に_app.tsx(js)
という名前でファイルを作成することで、標準設定をオーバーライドします。
このカスタムアプリを読み込んだ後にpages配下を読み込んでくれるので、ここに共通レイアウトを設定すればOKです。
import { AppProps } from "next/app";
import Layout from './Layout';
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
};
export default MyApp;
カスタムアプリとは別にカスタムドキュメントというものがありますが、共通レイアウトなどを定義する時には使用してはいけません。
https://nextjs.org/docs/advanced-features/custom-document#caveats
こちらにも注意書きとして記載がありますが、クライアントサイドでのイベント処理が機能しません。
カスタムドキュメントはheadタグやbodyタグなどの基本的なドキュメント定義をオーバーライドしたい時に使用します。
4. ReduxのStore設定方法
Reduxはstoreの設定方法が異なるだけで、使い勝手に特に違いはありません。
それぞれのstoreの設定方法を整理してみます。
RootReducerはこんな感じのものを使用と仮定。
import { combineReducers } from "@reduxjs/toolkit";
import TopSlice from "./modules/TopSlice";
import HogeSlice from "./modules/HogeSlice";
import FugaSlice from "./modules/FugaSlice";
const RootRedcuer = combineReducers({
top: TopSlice.reducer,
hoge: HogeSlice.reducer,
fuga: FugaSlice.reducer,
});
export type RootState = ReturnType<typeof RootRedcuer>;
export default RootRedcuer;
Storeはこんな感じ
import { configureStore } from "@reduxjs/toolkit";
import RootRedcuer from "./RootRedcuer";
const Store = configureStore({
reducer: RootRedcuer,
});
export type Dispatch = typeof Store.dispatch;
export default Store;
create-react-appの場合
こちらもまたまたMainApp.tsxを修正してあげればOKです。
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Provider } from "react-redux"; // ←←←←←←←←←← これを追加
import Store from "./Store"; // ←←←←←←←←←← これを追加
import Layout from './Layout';
import TopContainer from './containers/TopContainer';
import HogeContainer from './containers/HogeContainer';
import FugaContainer from './containers/FugaContainer';
const MainApp: React.FC = () => (
<Provider store={Store}>
<Router>
<Switch>
<Layout>
<Route exact path="/" component={TopContainer} />
<Route path="/hoge" component={HogeContainer} />
<Route path="/fuga" component={FugaContainer} />
</Layout>
</Switch>
</Router>
</Provider>
);
export default MainApp;
Next.jsの場合
こちらも同じようにカスタムアプリを修正すればOKです。
import { AppProps } from "next/app";
import { Provider } from "react-redux"; // ←←←←←←←←←← これを追加
import Store from "./Store"; // ←←←←←←←←←← これを追加
import Layout from './Layout';
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
return (
<Provider store={Store}>
<Layout>
<Component {...pageProps} />
</Layout>
</Provider>
);
};
export default MyApp;
Next.jsでSSR、SSGの方法
create-react-appとNext.jsの比較という記事の内容からは少し逸れますが、Next.jsでSSR、SSGする方法について軽く触れておきます。こちらは当然create-react-appにはない機能です。
公式ドキュメント
https://nextjs.org/docs/basic-features/data-fetching
基本的にpages
ディレクトリ配下に作成したページに以下の関数を追加するだけです。
- SSGしたい場合
- APIなどから取得したデータを元にページを動的に作成する場合・・・
getStaticPaths
+getStaticProps
- 動的なページ作成は不要な場合・・・
getStaticProps
のみ
- APIなどから取得したデータを元にページを動的に作成する場合・・・
- SSRしたい場合・・・
getServerSideProps
以下はSSGのAPIなどから取得したデータを元にページを動的に作成する場合
のサンプルコードです。(残りは公式ドキュメントを参考にしてください。)
import React from 'react';
import { GetStaticPaths, GetStaticProps } from 'next';
// 型定義:一覧
type Piyo = {
id: string;
};
// 型定義:詳細
type PiyoDetail = {
id: string;
name: string;
value: string;
};
// 3. あとはgetStaticPathsとgetStaticPropsで作成されたpropsを使用して画面描画する処理を記述してあげればOK
const piyoPage: React.FC<PiyoDetail> = ({ id, name, value }) => (
<div>
{`idは${id}、nameは${name}、valueは${value}です。`}
</div>
);
// 1. まずはここで動的に作成するページデータを取得
export const getStaticPaths: GetStaticPaths = async () => {
// API叩いて一覧データを取得と仮定
const response: Piyo[] = await fetch('https://.../piyos');
// paramsの中のid:はファイル名の[id]の部分と合わせること
const paths = response.map((piyo: Piyo) => (
{ params: { id: piyo.id } }
));
// これでgetStaticPropsでparamsが使用可能に
// fallback=falseは存在しないページは404
return { paths, fallback: false };
};
// 2. ここでpiyoPageでpropsとして使用するデータを取得・作成します
export const getStaticProps: GetStaticProps = async ({ params }) => {
// API叩いて詳細データを取得と仮定
const piyoDetail: PiyoDetail = await fetch(`https://.../piyos/${params.id}`);
// これでpiyoPageでpropsとして使用可能に
return { props: { piyoDetail } };
};
export default piyoPage;
これでawait fetch('https://.../piyos')
で1〜3のidが取得できた場合、
ビルド後に
http://localhost:3000/piyo/1
http://localhost:3000/piyo/2
http://localhost:3000/piyo/3
といった感じでアクセスできるようになります。
どちらを使用すべきか?
まずReactに触れること自体が目的の場合は、create-react-appで十分だと思います。
create-react-app → Next.jsの順番で学習した方がSSR・SSGなどNext.js自体の機能理解が深まる気がします。
あとは最初のSPA、SSR、SSGの話に戻って開発するサービスの要件や仕様に合わせての選定で良いと思います。
create-react-appが良いと思うケース
- SEO対策が不要なサービス
- 管理画面などログインが必要なサービス(ビルド時にデータ取得ができない かつ 特にSSRする必要もない場合)
Next.jsが良いと思うケース
- SEO対策が必要なサービス
- ページごとにSSR、SSGが必要なサービス(ECサイトを例にすると、頻繁にバックエンドのデータが変わる商品ページはSSR、利用規約などいつ見ても同じページはSSGといった使い分けをしたい場合)
- バックエンドから取得するデータが頻繁に変わらないサービス・・・基本的にSSGで開発して良いですがデータが変わるとビルドが必要になるため、データ更新のタイミングでビルド&デプロイを行うCI/CDの仕組みを構築しておくと尚良いと思います。
- バックエンドからデータ取得が不要なサービス・・・SPAで作っても良いと思いますが、SSGのメリットの方が大きい気がしてます。
まとめ
Next.jsは当然クライアントサイドでのレンダリングも可能なため、あくまで個人の見解ですが、迷ったらとりあえずNext.jsというのは全然アリだと思います。実質SPA、SSR、SSG全てカバーしていると言えるのかなと。
ただ、vercelなどのホスティングサービスを利用せず自前でデプロイ先のサーバーを準備する場合、SSRの有無によってインフラ周りで考慮する内容が変わってくるため、その辺りも判断基準に入れておくとべきかと思いました。