Next.jsを少しずつ理解していく12【SSR/サーバーサイドレンダリング】

今回からはNext.jsの2つ目のレンダリング型である「SSR」について紹介します。

Next.jsのSSGとSSR

Next.jsにはSSGとSSRという2種類のレンダリング型があることは、以前の記事で解説しました。

 

SSGとは、ビルド時にサーバー側で全てのページのレンダリングを行い、生成したHTMLなどのファイルをCDNにキャッシュさせることで、SSRよりも高速なページ表示を実現する方法です。CSRと比較してSEOにも強いという特徴があります。

 

SSRとは、リクエスト毎にサーバー側でレンダリングを行い、そこでHTMLなどのファイルを生成してクライアントに送信することでページを表示する方法です。こちらもSEOには強いですが、SSGに比べると表示速度が遅いとされています。

SSG/静的サイトジェネレーターの問題点

ここまでは、SSGの良いところに目向けて学習してきましたが、そろそろ欠点も把握しておく必要があります。

前提として、SSRは比較的大規模なサイト/アプリケーションにて利用されるものだと考えると、以降の説明がすんなり入ると思います。

頻繁にデータ更新を行えない

たとえば大規模なニュースサイトをSSGで作る場合を考えてみます。ニュースサイトでは毎秒ごとに新しい記事が更新され、コメント、評価、SNSシェア情報など、個人ブログなどとは比較にならないほど頻繁にデータが更新されます。

このようなアプリケーションを作成するのに、ビルド後はデータを更新できない「getStaticProps」を使用するのは不適切です。また、「getStaticPaths」でfallback「true」や「'blocking'」を指定すれば、一度めのリクエストでは最新の情報が表示されますが、それ以降は古いデータがキャッシュから届けられます。あるいはISRを使用すれば一度ビルドした後でも静的に生成されたページをアップデートすることができます。しかし、ISRがページを再更新するには短くても1秒の間隔が必要になります。例えば月間数百万に利用されるようなサイトでは、1秒間に数十人~数百人のユーザーが同時にサイトが閲覧しているわけなので、彼らに対して常に最新のニュースを提供できるとは限りません。

表示するデータをパーソナライズできない

例えばTwitterのように、ログインしているユーザーによってタイムラインに表示される内容が異なるような(表示・データがパーソナライズされる)アプリケーションを作る場合を考えてみます。

Twitterアプリでも使用されているReactなどのCSRで作る場合は、ユーザーのリクエストごとにユーザーの識別データ(userIdなど)を用いて都度データをフェッチし、それを用いてページを生成。といった処理を行います。

しかしSSGは、一度生成されたページはキャッシュされ、リクエストに応じてキャッシュからページファイルが届けられる仕組みです。リクエストごとにユーザの識別データを確認したり、それに応じてデータをフェッチして表示を更新することはできません。

SSR/サーバーサイドレンダリングのメリット

SSRは、ビルド時ではなくユーザーからのリクエスト時に初めてページをサーバーサイドでレンダリングします。リクエスト毎にレンダリングを行うので、常に最新のデータを表示できますし、ユーザーIDに応じて表示を切り替えるといったことも可能になります。

ちなみに、似たようなことはReactなどのCSRを用いれば再現できますが、CSRを採用するということはSEOの側面に大きな欠陥を作ることを意味します。

SSRはサーバーサイドでレンダリングされるため、SSGと同様にSEOに強く、表示速度も高速。というメリットがあります。

SSRを使ってみよう

さて、では実際にSSRを使用してページを生成してみましょう。

環境を整える

まずは、ISRについて紹介した記事でも使用したjson-serverというパッケージを利用して、新たにフェッチするデータを作りましょう。

 

プロジェクトルートディレクトリ(pre-rendering)直下のdb.jsonを開いて、以下のデータを追加します。追加したらターミナルを開いて、npm run server-jsonコマンドを実行してください。

pre-rendering/db.json

"news": [
        {
            "id": 1,
            "title": "News Article 1",
            "description": "Description 1 です",
            "category": "sports"
        },
        {
            "id": 2,
            "title": "News Article 2",
            "description": "Description 2",
            "category": "politics"
        },
        {
            "id": 3,
            "title": "News Article 3",
            "description": "Description 3",
            "category": "sports"
        }
    ]

Terminal

npm run server-json

結果(http://localhost:4000/news)

 

続いてはpagesディレクトリ直下にnewsディレクトリを作成して、その中にindex.jsファイルを設置します。まずは「List of News Articles」とだけ表示されるようコードを書いていきます。

Terminal

mkdir pages/news
touch pages/news/index.js

pages/news/index.js

function NewsArticleList({articles}) {
    return(
        <>
        <h1>List of News Articles</h1>
        </>
    )
}

export default NewsArticleList

Terminal

npm run dev

結果(http://localhost:3000/news)

SSRを使ってみる

SSRを使ってデータをフェッチするには、これまで使用していたgetStaticProps関数ではなくgetServerSideProps関数を用います。データの取得方法やPropsの渡し方についてはこれまでと変わりありません。

それではgetServerSideProps関数を用いてデータをフェッチし、mapメソッドで取得した一覧データを表示するところまでやってみましょう。

 

pages/news/index.js

//propsを受け取る
function NewsArticleList({articles}) {
    return(
        <>
        <h1>List of News Articles</h1>
        //map関数でデータを展開
        {articles.map((article) => {
            return(
                <div key={article.id}>
                    <h2>
                        {article.id} {article.title} | {article.category}
                    </h2>
                </div>
            )
        })}
        </>
    )
}

export default NewsArticleList

//getServerSideProps関数
export async function getServerSideProps() {
    //先ほど作成したデータをフェッチ
    const res = await fetch('http://localhost:4000/news')
    const data = await res.json()

    return{
        //propsを渡すのを忘れない
        props: {
            articles: data,
        }
    }
}

結果

ページが表示されましたね。開発者ツールでNetworkを確認すると、サーバーからnewsページのファイルが帰ってきていることを確認できます。

SSRでダイナミックルーティングを行う

続いては、SSRを使ってダイナミックルーティングを行ってみましょう。getStaticProps関数を使ってダイナミックルーティングを行う場合とどのように違うのかを意識しながら作業してみてください。

 

まずはnewsディレクトリの中に[category].jsファイルを作成し、リクエストされたパス名に応じて、その名前のカテゴリー名をもつ記事のみを表示できるようにしていきましょう。

Terminal

touch pages/news/[category].js

pages/news/[category].js

function ArticleListByCategory({articles, category}) {
    return(
        <>
        <h1>Showing news for category <i>{category}</i></h1>
        {articles.map((article) => {
            return(
                <div key={article.id}>
                    <h2>
                        {article.id} {article.title}
                    </h2>
                    <p>{article.description}</p>
                    <hr/>
                </div>
            )
        })}
        </>
    )
}

export default ArticleListByCategory

export async function getServerSideProps(context) {
    const {params} = context
    const {category} = params
    const response = await fetch(
        `http://localhost:4000/news?category=${category}`
    )
    const data = await response.json()

    return {
        props: {
            articles: data,
            category,
        },
    }
}

結果(http://localhost:3000/news/sports)

[category].jsとは?

以前も解説した通り、ファイル名を[]で囲った場合は、リクエストされたパスに応じて動的にページを生成する「ダイナミックルーティング」を行うファイルになります。

つまり、newsディレクトリの中にダイナミックルーティングファイルを用意すると、/news/sportsや/news/politicsなど、/newsに続くパス名に応じて動的にページが生成されるということです。

contextパラメータとは?

contextパラメータとは、paramsやpreviewsなどのキーが含まれたオブジェクトです。例えばparamsキーには、ダイナミックルーティングのページのルートパラメータが含まれています。つまり、ページのファイル名が[category].jsの場合、paramsキーの中身は{category : ...}のようなオブジェクトになっているため、例えばparams.categoryとすることで、sportspoliticsなどの値(動的にリクエストされたパス名)を取り出すことができます。

詳しくは公式のドキュメントを参照ください。
ServerSideProps|Nextjs.org

SSRのcontextを使ってみる

以前も紹介した通り、contextパラメータはgetStaticProps関数(SSG)でも使用できます。特によく使用するのがparamsキーで、params.ページファイル名とすれば、動的に生成されたページのパス名を取り出すことができます。

さらに、getServerSideProps関数でcontextパラメータを使用する場合、reqとresというキーも利用できます。※reqキーとresキーの詳しい使い方については、nodejsの公式ドキュメントを参照ください。

Data Fetching: GetServerSideProps|Nextjs.org

 

今回はこのresとreqを使用して、cookie情報を更新・取得する方法を試してみたいと思います。

まずは、resのsetHeader関数を使用してCookie情報を追加してみましょう。getServerSideProps内を次のように変更してください。

pages/news/[category].js

export async function getServerSideProps(context) {
    //contextからreqとresを取り出す
    const {params, req, res} = context

    //req.headers.cookieでcookie情報を取得表示
    console.log(req.headers.cookie)

    //setHeader関数を使ってcookieを設定
    //第一引数でヘッダーの種類を指定
    //第二引数でセットする値を指定
    res.setHeader('Set-Cookie', ['name=Vishwas'])

    const {category} = params;
    const response = await fetch(
        `http://localhost:3001/news?category=${params.category}`
    )
    const data = await response.json()

    return {
        props: {
            articles: data,
            category,
        },
    }
}

結果(http://localhost:3000/news/sports)

開発者ツールでcookieを確認すると、cookieのNameが「name」、Valueが「Vishwas」としてセットされていることが分かります。

また、Terminalの表示を確認すると、undefinedと表示されていると思います。これは、一度目のリクエストの際にはcookieが設定されていなかったためです。

もう一度ページをリロードしてからTerminalを確認すると、一度目のリクエストの際に設定したcookie情報を読み取ることが出来るはずです。

このようにCookieを使用することで、ユーザーごとに個別の識別子を与え(設定し)それをもとにユーザーを識別することでパーソナライズされたデータをフェッチしたり、表示を切り替えたりすることができるようになります。

その他にもCookieの利用には様々なアプローチがあり、どのように活用するかは完全に開発者の自由です。

SSRのqueryを使ってみる

contextパラメータに含まれるキーの中には、resとreqの他にもう一つ、SSRでしか使えないqueryキーというものがあります。queryキーを使うと、動的に生成されたページのURLに含まれたクエリを取得することができます。

 

実際に使ってみましょう。まずはcontextからqueryキーを取り出して、console.logでTerminalに出力できるように変更します。

 

pages/news/[category].js

export async function getServerSideProps(context) {
    //queryを取り出す
    const {params, req, res, query} = context
    //console.log(req.headers.cookie)
    res.setHeader('Set-Cookie', ['name=Vishwas'])

    //queryをターミナルに表示
    console.log(query);
    const {category} = params;
    //console.log(params);
    const response = await fetch(
        `http://localhost:3001/news?category=${params.category}`
    )
    const data = await response.json()

    return {
        props: {
            articles: data,
            category,
        },
    }
}

結果(http://localhost:3000/news/sports)

ページをリロードすると、ターミナルに{category: 'sports'}と表示されます。これもqueryの使い方の1つですが、ここまではparamsとさほど変わりませんね。

では、URLに?subcategory=footballというクエリを追加した場合はどうでしょうか?

結果(http://localhost:3000/news/sports?subcategory=football)

今度はcategoryだけでなく、クエリに指定したsubcategory'football'という値も取り出すことが出来ました。

SSRをビルドする

最後にビルドを行って、これまでに作成したSSRのページがどのように動作するのかを確かめてみましょう。

まずはpre-renderingディレクトリ直下にある.nextディレクトリを削除した後、ターミナルでnpm run buildnpm run startを実行します。

Terminal

rm -r .next
npm run build
npm run start

結果

newsディレクトリのページはSSRが設定されているため、ビルド時にHTMLなどのページファイルは生成されません。SSRはリクエスト毎にデータをフェッチし、都度ページファイルを生成する仕組みです。

では試しに、db.jsonの値を変更して、SSRが変更したデータをフェッチして新たなページファイルを生成できるのか試してみましょう。

変更前(http://localhost:3000/news)

変更後(http://localhost:3000/news)

上手く機能しましたね!

getServerSideProps関数の決まり事

getServerSideProps関数を扱う上で知っておいた方が良い重要な決まり事をいくつか紹介します。

1.サーバーサイドで実行される

getServerSideProps関数は必ずサーバーサイドで実行されます。クライアントサイド(ブラウザ)で動くことはありません。

また、getServerSideProps関数内に記述したコードは、ブラウザに送られるJS bundleにも含まれることはありません。(propsに渡したデータ等はクライアントサイドで取得可能な場合があります)

2.サーバーサイドの処理を記述できる

getStaticProps関数と同じく、getServerSideProps関数にもサーバーサイドの処理を直接記述することができます。例えばfsモジュールで使用したファイルの読み書きや、データベースのクエリ操作なども可能です。

また既述の通り、getServerSideProps関数内に記述したコードはクライアントに送信されることはないため、APIキーなどの機密性の高いデータを含めても問題ありません。

ただしreturn{}内に記述したpropsのデータはクライアントに送信されてしまう恐れがあるため、APIキーなどの機密情報を含めないように注意が必要です。

3.pageファイルのみで利用できる

getServerSideProps関数はpageファイルのみで利用できます。pageファイルとはpageフォルダ内に格納されているファイルのことです。

よって、例えば通常のcomponentファイルからは利用できません。

4.Propsを返さないといけない

既述の通りですが、getServerSideProps関数は必ずprops keyを含んだオブジェクトを返さなければなりません。また、propsとして返す値もjsオブジェクトでなければなりません。

5.リクエスト毎に実行する

getServerSideProps関数はリクエストがあるたびに実行されます。つまり、リクエストのたびにデータをフェッチしてページファイルを生成します。この点がgetStaticProps関数との大きな違いです。

都度データをフェッチするgetServerSideProps関数を使用することで、常に最新のデータを取得できます。

まとめ

以上でNext.jsのSSRについての紹介は終わりです。

 

次回はクライアントサイドでのデータ取得について紹介します。

 

コメントを残す

CAPTCHA