🌟

NEXTでSSGしてfirebaseにデプロイしよう

6 min read


みなさん、NEXTで作ったアプリはどこにデプロイしていますか?
やっぱりVERCELですか?
VERCELはNEXTとの相性が良いのでNEXTアプリのデプロイ先として真っ先に候補にあがると思います。
しかし、VERCELの無料プランは個人かつ非商用の制限があります。
どこまでが商用かは明確には決まっていないようです。
個人ブログに広告を載せるのはOKという情報もありました

しかし、ECサイトを作りたい!個人事業のHPを作りたい!アフェリエイト広告を載せたい!など、商用に分類されそうなグレーなケースは出てくると思います。
VERCELのproプランは $20/month で決して安くはありません。

そこで今回は安価かつ簡単にデプロイ可能なfirebase HostingでNEXTアプリをリリースする方法を紹介していきます。
今回は SSG(Static Site Generation) を使って静的なサイトとしてデプロイします。

なぜSSGか

結論から述べるとシンプル、安価、ハイパフォーマンスにデプロイ可能だからです。

NEXT(React)でデプロイするにはいくつか選択肢があります。
SSG以外だとCSR(Client Side Rendering)、SSR(Server Side Rendering)が一般的だと思います。しかしこの2つにはいくつかデメリットがあります。

CSRのデメリット

CSRはその名の通りレンダリングをクライアントサイドで行います。
初回アクセス時にレンダリングに必要なファイルをすべてダウンロードしてクライアントでレンダリングします。そのため、初回アクセスから画面が表示されるまで時間がかかるデメリットがあります。

しかし1番大きいデメリットはやはりSEOだと思います。サーバーにはレンダリングされたHTMLはないのでクローラーがうまく認識してくれない可能性があります。最近のクローラーは賢くなってレンダリング結果まで見てくれるという話もありますが、実際どの程度SEOが改善されているかは怪しいです。そのためできるならSEOのためにレンダリングしたHTMLをサーバーに置いて置きたいです。

SSRのデメリット

SSRもその名の通りサーバーサイドでレンダリングをします。
クライアントからのリクエストがあるとサーバーサイドでレンダリングを行ってHTMLを返します。サーバー上でHTMLが生成されるのでSEO問題が改善されます。また初回アクセスもCSRと比べて高速です。

一見完璧に見えますがデメリットはあります。
それはレンダリング用にサーバーを用意しなくてはならないということです。
VERCELを使えばそこまで大変ではなさそうですが、firebaseなどの静的ホスティングサービスを利用する場合、別途レンダリング用のサーバーを用意する必要があります。
これは個人的にかなり大きなデメリットだと思います。できればアーキテクチャをシンプルに保つためにあまり使用するサービスを増やしたくないですよね。さらにレンダリングサーバーの追加料金も発生する可能性があります。

そこでSSG

どうやらレンダリングがボトルネックになっているようです。そこで、
レンダリング?そんなのデプロイ時にやってしまってサーバーに生成されたHTMLを置いておけばいいじゃん!そんな考えで生まれたのがSSGです。SSGではレンダリングはデプロイ時に行うのでレンダリングサーバーを用意する必要がありません。なのでfirebaseなどの静的サイトホスティングサービス単体でも使えます。さらにクライアントからのリクエストが来たときにはそのままHTMLを返すだけなのでSSRよりも高速です。
SEO、パフォーマンス、ランニングコスト、全てにおいて完璧なSSG。これは使うしかない!

当然SSGにもデメリットはある

SSGではデプロイ時にHTMLファイルを生成します。なので外部データに更新があったときにその変更がリアルタイムに反映されません。更新するには再度デプロイする必要があります。
なのでTwitterのようなユーザが頻繁に投稿するサービスには当然使用できません。
ですが、更新がそこまで頻繁ではない場合、SSGで十分だと思います。例えばオリジナルECサイトをSSGで作ったとします。商品を追加したくなったときはその時に再度デプロイ時すればいいのです。
個人ブログなども投稿時にデプロイする処理を自動化しておけば十分SSGで対応できると思います。
公式もSSG推しのようです。(https://nextjs.org/docs/basic-features/pages#static-generation-recommended)

NEXTでのSSGのやり方

NEXTのexportを使うだけで静的HTMLがoutディレクトリ下に生成されます。なのでこれをデプロイするだけです。以下はfirebaseでのやり方です。

package.json
"scripts": {
  "build": "next build && next export"
}
npm run build
firebase deploy

基本的にこれだけでデプロイできます。しかしいくつか設定項目があるのでそれらを説明していきます。

NEXTの設定

NEXTで /pages/about/index.tsx をエクスポートすると out/about.html として書き出されます。これをfirebaseで読み込み可能にするために out/about/index.tsxに書き出す必要があります。そこでnext.config.js の trailingSlash を設定します。

next.config.js
module.exports = {
  trailingSlash: true,
};

firebaseの設定

以下のようにデプロイするディレクトリをoutに設定します。

firebase.json
{
  "hosting": {
    "public": "out",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

また今回はCSRではなくSSGなので以下の設定があれば消して起きましょう。
これはfirebase initしたときにSPAを使うかと聞かれたときにYESと答えると自動で記述される項目です。

firebase.json
{
  "hosting": {
    "public": "out",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
    /* SPA用の設定なので不要
    "rewrites": [{
       "source": "**",
       "destination": "/index.html"
     }]
     */
  }
}

動的なコンポーネントの設定

SSGではデプロイ時にレンダリングを行うので外部データを取得する設定が必要があります。
それを行う関数がgetStaticPaths()getStaticProps() です。以下は公式レポジトリ(https://github.com/vercel/next.js/tree/canary/examples/with-static-export)の例です。
getStaticPaths() で必要なパスを列挙します。そして実際にそのパスのぺージに与えるデータを getStaticProps() で取得します。これはデータが外部にある場合の例になっています。

pages/post/[id].js
import Link from 'next/link'
import Head from 'next/head'

export async function getStaticPaths() {
  const response = await fetch(
    'https://jsonplaceholder.typicode.com/posts?_page=1'
  )
  const postList = await response.json()
  return {
    paths: postList.map((post) => {
      return {
        params: {
          id: `${post.id}`,
        },
      }
    }),
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  // fetch single post detail
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`
  )
  const post = await response.json()
  return {
    props: post,
  }
}

export default function Post({ title, body }) {
  return (
    <main>
      <Head>
        <title>{title}</title>
      </Head>

      <h1>{title}</h1>

      <p>{body}</p>

      <Link href="/">
        <a>Go back to home</a>
      </Link>
    </main>
  )
}

データが内部(ホスティングサーバー上)にある場合

上の例ではデータが外部にある場合でしたが、内部にある場合はどうしたらいいでしょうか?データを他のサーバーから取得する処理は当然いらないです。しかし、そのデータを用いて動的パスを設定している場合は注意が必要です。

例えば以下のように記事のデータをフロントで持っていた場合。レンダリング時にどのようなパスが生成されるべきかを明示的にNEXTに教えてあげる必要があります。

constant/postList.js
export const postList = [
  {
    id: '01',
    title: 'post_01',
    body: 'this is 01 post',
  },
  {
    id: '02',
    title: 'post_02',
    body: 'this is 02 post',
  },
  {    
    id: '03',
    title: 'post_03',
    body: 'this is 03 post',
  }
]

このような場合、普通にデータを読み込んでパスを列挙するようにgetStaticPaths()を定義すれば大丈夫です。getStaticPropsは以下のように何も渡さないように定義すれば問題ありません。

pages/post/[id].js
import { articleList } from '../constants/store';

export function getStaticPaths() {
  const paths = articleList.map((elem) => ({
    params: { id: elem.id },
  }));
  return { paths, fallback: false };
};

export function getStaticProps() {
  return { props: {} };
};

以上でfirebaseにデプロイする設定は終了です🎉
お疲れ様でした!

まとめ

様々な事情でVERCELをデプロイサーバーとして使えないときのためにfirebaseへデプロイする手順をまとめてみました。
SSGと組み合わせることでシンプル、安価そしてハイパフォーマンスにデプロイすることができます。本記事で紹介した方法は更新を頻繁にしないサイトであれば適用できます。
ぜひfirebaseのデプロイも検討してみてください!

参考

https://nextjs.org/
https://tech.012grp.co.jp/entry/2021/03/25/125014
https://zenn.dev/lollipop_onl/articles/eoz-vercel-pricing-2020#料金
https://blog.kimizuka.org/entry/2021/01/16/203050
https://firebase.google.com/docs/hosting/full-config?hl=ja
https://qiita.com/jagaapple/items/faf125e28f8c2860269c

Discussion

ログインするとコメントできます