Next.jsの特徴 – Reactとの比較

Next.jsとはVercel社が作ったReactのフレームワークです。単体のReactより機能が拡張されています。Reactの特徴は全て引き継がれ、コードの書き方もReactそのものですので、Reactユーザーであれば難なく使うことができます。本稿ではNext.jsの概要について書きたいと思います。

Next.jsの特徴

ReactにはないNext.jsの代表的な特徴として次のようなものがあります。

  1. バックエンドとフロントエンドが同じサーバで動く
  2. SSG、SSR、CSRの使い分けが可能
  3. React Routerを使わずとも、Router機能が使える
  4. <Head>コンポーネントが使える
  5. Reactよりセットアップがずっと早い
  6. Image OptimizationとInternationalizationが使える
  7. Tailwind CSSがデフォルト

バックエンドとフロントエンドが同じサーバで動く

フルスタックのWebアプリを作ろうとすると、フロントエンドサーバーとは別にバックエンドサーバーを立てる必要がありますが、Next.jsでは一つのサーバで両者が動いています。具体的には、Next.jsのプロジェクトを立ち上げると、pagesフォルダ内にapiというサブフォルダがあり、その中のファイルは全てバックエンドとなります。初期設定でhello.jsというファイル(ボイラープレート)がありますので、同ファイルの内容をコピーして始めるとスムーズです。ボイラープレートを見るとExpress.jsとはまるで違うので戸惑うかもしれませんが、ボイラープレートの中の記法はExpress.jsとほぼ同じです。バックエンドのエンドポイントは、localhost:3000の場合では、localhost:3000/api/helloになります。



但し、Next.jsのバックエンドはステートレスです。すなわちリクエストが来る都度サーバーインスタンスを立てて処理を実行し、レスポンスを返すとインスタンスが停止します(メモリが解放される)。サーバーインスタンスを常時稼働させたい場合は、Express.jsをNext.jsのプロジェクト内で動かすことをお勧めします。

Static Site Generation(SSG)とServer-side Rendering(SSR)

SSG又はSSRの実装がない外部APIを伴うサイトをユーザーが訪れると、ユーザー側(クライアント側)で外部API等からのデータをリクエストしながらレンダリングする必要があるので、読み込みと表示に時間差が発生します。下記をご覧ください。ユーザーからのリクエスト①を受けたフロントエンドサーバーは、②HTML, CSS, JavaScriptを返して役目を終え、ユーザーの側では③フロントエンドサーバーから受けた内容だけで表示できるものがまず表示されます。③と同時に外部APIにリクエスト④を出し、レスポンス⑤を経てやっと⑥に至り外部APIのデータがユーザーに表示されます。この③と⑥の時間差が表示の時間差の原因です。JavaScriptで作ったサイト又はReactで構築したサイトは常にこの挙動です。SSG, SSRに対してClient-side Rendering(CSR)と呼ばれます。

外部APIとの通信に時間を要するのは仕方がないものの、であるのならば時間差なしすべての情報が揃ってからまとめて表示される方がユーザー側としては快適です。また、この時間差はSEOの観点からもマイナスです。Googleのロボットがホームページを訪れたときは外部APIからリクエストを待たずに、初回表示の内容を重視すると言われます。

この時間差問題を解決するのがSSGとSSRです。いずれも初回読み込み時に必要なデータをサーバー側で揃えてから、ユーザーには完成された最小限のHTML、CSS、JavaScriptだけを返します。次のようなイメージです。⑤の部分で時間差が生じません。

初回読み込み後は、SSRではユーザー(クライアント)からのリクエストの都度、サーバー側で毎回レンダリングをしてクライアントに返します。他方SSGではリクエストの都度、サーバーでレンダリングをせず、クライアントはCDNのキャッシュを使います。Next.jsの公式サイトのこちらのページではSSGを推奨しており、具体例含めて大変参考になります。

Next.js ver13のappディレクトリー構成

2022年10月にリリースされたNext.js ver13では、従来からあるpagesディレクトリー構成の他に、appディレクトリー構成も使えるようになしました。appディレクトリーの内部ではサーバーサイドでページを生成するStatc Rendering(SSGとSSRの総称)がデフォルトになり、明示的に指定したときに限りクライアント側でページを生成するClient-side Rendering(CSR)が使えます。CSRの指定は、1行 ‘use client’と書くだけで済みます。

appディレクトリー構成は、サーバーで必要な処理してクライアントには出来上がった静的ファイルだけを返すという思想ですので、前節で図示したようなクライアント側で状態を保持しながらら画面をインタラクティブに操作するReactのCSRの思想とは根本的に異なります。重いapiの処理を実装する予定がなく、Reactに近い使い方をしたい場合、あるいはReactを経由したNext.jsの入門者は、Next.js ver13のappディレクトリー構成ではなく、CSRがデフォルトのpagesディレクトリー構成をお勧めします。同じNext.jsですが、両者の思想を政治に例えるなら右と左くらい遠いです。

Router機能が内蔵

ReactはSingle Page Applicationに最適化されており、複数のページを備えようとすると以前の私のこちらの記事で書いたようにReact Routerというライブラリーを使う必要があります。Next.jsでは、Reactの仮想DOM機能そのままに、Reactよりもずっと簡単に複数のページを持つWebアプリ(サイト)を作ることができます。

具体的にはNext.jsのプロジェクトを立ち上げるとpagesというフォルダがあるので、その中にReactのコンポーネントファイルを置くだけでページが作れます。ファイル名がそのままパスになりますので、先頭の文字はReactのコンポーネントのように大文字ではなく小文字で始めます。

上の例では、login.jsというファイルを作成していますので、localhost:3000/loginというパスで、login.jsの内容が表示されます。階層を深くするにはpagesの中にサブフォルダを設置、その中にReactのコンポーネントを設置します。上記の例ではchatというフォルダがありますので、chatの中にlogin.jsを配置すると、localhost:3000/chat/loginがパスになります。

Index.jsを使ったRoutingの記法

Next.jsにおいてindex.jsファイルは特別な意味を持っています。ルートにもindex.jsはありますが、先ほどのchatという名称のサブフォルダの中に、login.jsではなくindex.jsというファイルを配置すると(ファイル名を変えるだけで中身は同じ)、パスはlocalhost:3000/login/indexとはならず、localhost:3000/loginになります。Next.jsのpages dir構成ではいずれの記法でも動きますが、Next.jsのapp dir構成ではこのindex.jsを使う記法しか使えないので、pages dir構成を採用する場合でも、index.jsを使った記法に慣れることをお勧めします。

その他のコンポーネントファイル

pages dirの外側では、Routingとは無関係のReactコンポーネントも使用可能です。この例ではcomponentsフォルダ内に、Reactのコンポーネントを幾つか格納しています。pages内のファイルと異なり、こちらはReactコンポーネントですので、ファイル名の先頭は大文字にします。両者の使い分けは、後ほどユーザー認証の実装の部分でも簡単に解説します。

サイト内のページ遷移にはReact Routerと同様の<Link>というコンポーネントが使用できます。<Link>は仮想DOMと差分レンダリングで動くので、<a>タグを使うよりずっと高速です。

<Head>コンポーネントが使える

Reactアプリケーション内に複数ページを持たせるのみならず、SEOも考慮してそれぞれのページに固有のヘッダー(メタ情報)を持たせたいことがあります。ReactのRouterでは、ページ毎に固有のヘッダーを持たせることが難しいのですが、Next.jsではJSX内に<Head>コンポーネントを設置することで容易にヘッダーを設置できます。

具体的には次のコードをご覧ください。1行目でHeadをインポートし、<Head></Head>コンポーネントの内側でページのタイトルをAboutとしています。なおここでは<Head>以外の実際の表示部分を全て別途作成のAboutContentコンポーネントから読み込んでいます。

abut.js

import Head from “next/head”;
import AboutContent from “..components/AboutContent”;

function About() {
return (
< div>
<Head>
<title>About</title>
</Head>
<AboutContent />
</ div>
);
}

export default About;

Reactよりセットアップがずっと早い

Next.jsは多機能のようで、バージョンアップするたびにファイルサイズが軽量化されていて、Reactよりも軽いとすら言えます。例えば、Reactのnpx create-react-appによるプロジェクトの立ち上げには毎回それなりの時間を要しますが、Next.jsのnpx create-next-appを使うとずっと早く立ち上がります。

Image OptimizationとInternationalization

ReactにないNext.jsに固有の機能は幾つかありますが、Image OptimizationとInternationalizationがとくに強みです。いずれも2020年のVer.10から新しく導入された機能です。

近年あらゆるWebサイトには画像が非常に多く使われ、サイトをReactなどで高速化したとしても画像の読み込みに時間を要し、画像がサイト高速化のボトルネックとなっています。Next.jsではHTMLの<img>タグではなく、<Image>コンポーネントを使うことでとくに何も考えずとも画像が最適化されます。<Image>コンポーネントはインポートが必要ですが、JSX内の使い方は<img>タグと全く同じです。

Internationalizationとは複数言語サイトのための機能です。Next.jsでは簡単にユーザーの使用する言語に応じて該当する言語のページを表示させることができます。各言語ページのパスの設定も容易です。

Tailwind CSSがデフォルト

2023年2月頃より、npx-create-next-app@latestコマンドでプロジェクトを始めるとTailwind CSSがデフォルトで選択できるようになりました。下記のWould you like to use Tailwind CSS with this project No/ Yesの部分です(Yesに下線)。Tailwind CSSはこちらの私の記事「Tailwind CSSの設定と実装方法 – v3.0準拠」で詳しくご紹介しています。


Next.js 13 Tailwind CSSがデフォルト

_app.jsの役割 – pages dir構成の場合

バックエンドとサーバサイドレンダリングが不要な場合でも、_app.jsの使い方さえ覚えればReactのようにNext.jsを使いながらそのメリット(上記③~⑦)を享受することができます(オススメ)。Next.jsのプロジェクトを立ち上げると、_app.jsというファイルとindex.jsというファイルがあります。

index.jsにはルートパス(”/”)に表示される内容を記述します。_app.jsはそれ自体表示されませんが、ルートパスを読み込む前の処理を記述できます。ルートパス表示前の処理ですので、ユーザーの状態によってはルートパス以外のページを始めに表示させることもできます(ログイン画面など)。Reactのapp.jsの役割が、Next.jsでは_app.jsとindex.jsに2分割されたと考えるとわかりやすいです。さらにhtmlタグの属性やHeadのメタ情報だけを管理する_document.jsというファイルを設置し、ReactのApp.jsの役割を3分割することも可能です。

次のセクションでは、以前こちらの記事でご紹介したuseAuthStateの認証機能をNext.jsの_app.js上で実装する方法をご紹介します。

Next.js + firebase + useAuthStateで認証機能を実装

下記のコードをご覧ください。1行目のglobal.cssはグローバルに適用されるcssファイルでNext.jsのプロジェクトを立ち上げると自動で作成されます。useAuthStateとauthの詳細は上記以前の記事をご覧頂ければと思いますが、Reduxのようなグローバル変数(実際にはユーザーのログイン状況)を格納するためのライブラリーと捉えて頂ければここでは十分です。LoginとLoadingの部分は別途用意したものを読み込んでいます。Loginは前述Router機能が内蔵の部分でも紹介した通り、_app.jsと同じ階層にありますので固有のページを割り当てていますが、ロード中にだけに示すLoadingは固有のパスを割り当てる必要がないので通常のReactのコンポーネントにしています。なお、Next.jsではReactを明示的にインポートする必要がありません。

関数コンポーネントの内側ですが、useAuthState関数でユーザーのログイン状況がuserに格納されます。

!userが真の場合、すなわちユーザーがログインしていない場合は<Login >コンポーネントを表示します。また、loadingが真の場合、すなわちloading中の場合は<Loading />コンポーネントを表示します。ユーザーがログインし、かつloadingが偽の場合には、最下段<Component />の部分が返されます。Componentが返されると、ルートパスに設置されるindex.jsが読み込まれる仕組みです。



_app.js

import “../styles/global.css”;
import { useAuthState } from “react-firebase-hooks/auth”;
import { auth } from “../firebase”;
import Login from “./login”;
import Loading from “../components/Loading”;

function MyApp({ Component, pageProps }) {
const [user, loading, error] = useAuthState(auth);

if (loading) return <Loading />

if (!user) return <Login />

return < Component {…pageProps} >
}

export default MyApp;

_app.js自体は表示を持てないので、このように条件に応じてコンポーネントをreturnをすることが_app.jsの主な役目です。その他、全ページ(コンポーネント)に共通の読み込み時前処理としてUseStateをここに記述することができます。

今日も最後まで読んで頂きありがとうございました。