この記事はREADYFOR Advent Calendar 2020の11日目の記事です。
こんにちは!READYFORでフロントエンドエンジニアをしております、江面 @neripark です。最近は @要素セレクタ警察 なんてあだ名で呼ばれたりもしています。CSS設計、コンポーネント設計、Netlify などが好きです!
さて、READYFORは最近、実行者様(クラウドファンディングで支援を募る方)向けの一部機能について、(まだ一部のユーザー様へのみではありますが)新規開発したSPAでの提供を開始しました。SPAの稼働は、社内では初の事例になります。
技術としては、タイトルにもある通り Next.js のSSGで生成した静的ファイル群を Netlify にホスティングしており、バックエンドとの通信は OpenAPI で定義したスキーマに従って行っています。
本記事では、この組み合わせで管理画面のフロントエンドを実装する上でのポイントとそのメリットについて、簡単にお話したいと思います!
事前に検討したこと
Netlify は個人で静的なウェブサイトのホスティングに使っていたり、社内でもStorybookのホスティングに使用していたので雰囲気は把握していましたが、管理画面のような動的なアプリケーションを実装するのは初めてでした。
それにあたり、いくつか調査・決定すべきポイントがありました。今回は2点に絞ってご紹介したいと思います。
ポイント1: 認証方法の決定
今回やりたいのは、Ruby on Rails で実装されたモノリシックなウェブアプリケーションから、管理画面の機能だけをSPAに切り出すというものです。
いきなり全機能を切り出すのは工数・リスクの観点から現実的ではないので、重要な機能から段階的に切り出していくこととしました。そのため、しばらくはRails側の機能とSPA側の機能が並行して稼働する形となります。
その際に必須となってくる要件が、「Rails側とログイン状態を同期する」だと思います。Rails側でログインを済ませたのに、SPA 側で再度ログイン操作を要求されたら、ユーザーとしてはちょっと煩わしいですよね。
今回は初期コストを削減するため、(Rails 側のユーザー認証でもともと devise
Gem を使っていたこともあり、)新たな認証サーバーのようなものは作らず、Rails 側から発行された Cookie を SPA 側でも使うこととしました。
SPA 側のホスト名はサブドメインを切ろうと決めていたので、Rails 側に SPA のドメインのみクロスドメインを許可する設定を行い、無事にログイン状態を同期できることを確認しました。
ポイント2: 動的なURLへの対処
Rails 側に合わせ、URL が同じなら同じ状態で復元される、いわゆる REST 的な URL でリソースを表現することも必須だろうと考えました。
もう少し具体的に言うと、ユーザーAさんが自分のプロジェクト(id: 1234)を編集を再開したいと思ったときに /projects/1234/edit
にアクセスすればいつでも前回までの状態を復元できたり、Aさん以外の人が /projects/1234/edit
にアクセスした場合は 404 エラー画面を表示させる、のようなことですね。
しかし、SSG の場合、全ユーザー分ファイルを生成するわけにもいきませんし、そもそもそれだと内容が動的になりません。そこで、Next.js の Dynamic Routing と Netlify のリダイレクト機能を使うことで対処しました。
Next.js の Dynamic Routing
Next.js は、v9.3 以上であれば src/pages/
配下にディレクトリを切って .jsx(.tsx) ファイルを置くだけで、自動でルーティングを生成することができます。
さらに、SSG をするとその通りにディレクトリを切って静的ファイルを出力してくれます。src/pages/project/1234/edit/index.html
であれば、out/projects/1234/edit.html
が出力されるといった具合です。
/projects/:id/edit
のような動的なURLを扱いたい場合は、src/pages/projects/[id]/edit/index.tsx
のようにディレクトリ名を[]
で囲って作成すると、その部分が動的なパスとして解釈されるようになります。
SSG 後のファイルは、out/projects/[id]/edit.html
となります。
これで、Next.js 側の準備は完了です。
※公式ドキュメントに詳しい記載があります。
Basic Features: Pages | Next.js
Netlifyのリダイレクト機能
Node.js サーバーで SSR 運用をするのであれば設定はこれだけで大丈夫なのですが、SSG の場合はこれに加えて、ホスティング先のサーバーにも設定が必要になります。
プロジェクトの編集画面が /projects/:id/edit
のようなパスだった場合、ユーザーからのリクエストは /projects/1234/edit
のように :id
の部分に具体的な値が入った形で届きますが、Next.js で出力した静的ファイルには /projects/1234/edit.html
のような :id の部分が具体的なファイルは物理的に存在していないため、何も設定していないと 404 になってしまうためです。
そこで、ホスティング先に「/projects/:id/edit
(:id のところは何が入るかわからない)へのリクエストが来たら、(Next.js で出力した)out/projects/[id]/edit.html
を返す」という設定をする必要があります。
Netlify の場合は、簡単な記述でその設定を行う方法が提供されており、今回はそれを使わせていただきました。
※公式ドキュメントに詳しい記載があります。
Redirects and rewrites | Netlify Docs
これに従い、リポジトリ直下の netlify.toml
に以下記述を行います。
# netlify.toml [[redirects]] from = "/projects/:id/edit" to = "/projects/[id]/edit.html" status = 200 force = false
※ Next.js の SSG 出力先である out/
は Netlify 側でドキュメントルートに設定しているので、ここでは記述していません。
これで、動的なURLを扱う準備はOKです。
あとは Next.js 側で :id を取得して、よしなに API コールを行っていくのみです。
感じているメリット
次は、管理画面のフロントエンド実装にこれらの技術スタックを選択するメリットをご紹介いたします。
※もちろん開発開始前にメンバー間で議論し、適切だと判断して決定しています。
メリット1: Node.js サーバーを運用する必要がない
動的なアプリケーションを提供したい、かつ、サーバーロジックと UI を疎結合にしたいと考えた場合、Node.js サーバーを立てて SSR するという選択肢もあると思いますが、今回は採用を見送りました。
SSR を採用すると、jsの実行環境ひとつとっても Node.js 側で行うかクライアント側で行うかの考慮が必要になり、そのぶん設計面も考えることが増えます。もちろん SSG でもその意識は必要なのですが、アウトプットが静的ファイルだとわかっているので、作り方がシンプルになるメリットがあると思います。
また、サーバー構築・運用はUIの構築とは別ベクトルのスキルであり、サーバーの経験豊富なエンジニアがフロントエンドにいなければ、バックエンドエンジニアやSREに工数を割いていただく必要が出てきます。
一方で、SSR のメリットを享受できるのはどんなケースなのかを考えると、SEO・OGP対策が重要である場合や、ブログやメディアサイトのような頻繁な更新が予想される場合などではないでしょうか?
作りたいものが管理画面であれば、どちらも該当しません。それならば、高い保守コストを払ってまで Node.js サーバーを運用する必要はないだろうと判断しました。
これが、管理画面の実装にSSGが適しているだろうと僕が思う、大きな理由の一つです。
メリット2: 各環境をフロントエンドエンジニアだけで用意できる
Netlify の以下3つの機能のおかげで、各環境の準備をらくらく&素早く行えました。
(1) テスト用URLの準備が超かんたん!
Netlify には、Deploy Preview と Branch Deployがあります。大好きな機能です☺️
開発をしていると、自分が実装した機能が正しく動くかどうかを、PdMやデザイナー、QA担当の方などに実際の画面を触ってテストしていただくことがあると思います。エンジニアが確認するだけならローカル環境で事足りますが、非エンジニアにそれを要求するわけにもいきません。
そんなとき、この Deploy Preview の機能が最適です。PullReq を作成すると、こんな形で、テスト環境用に自動でURLを切ってデプロイしてくれます。
あとは、このURLをテストしてくれる方に連絡するだけです。な、なんて簡単なんだ...。
また、Branch Deploy の機能を使えば、PullReq単位ではなく、ブランチ単位でのデプロイも可能です。今回のSPA立ち上げではQAを外部の会社さんにお願いしたのですが、その会社さん用の環境を別に用意しよう!といった要望にもおかげさまで柔軟に対応できました。
(2) 接続先APIサーバーの切り替えは環境変数で行える
テスト環境なら、当然ですが接続先APIサーバーもテスト用のものに切り替える必要があります。
Netlify ならそれにも簡単に対応できます。「行われているデプロイのコンテキストに応じて、環境変数の値を変える」ことが可能なため、それを使います。
netlify の設定に、環境別に環境変数を設定し、
# netlify.toml [context.branch-deploy] environment = { ENV = "development" } [context.production] environment = { ENV = "production" }
これを読み分けるコードを Next.js 側で用意すれば、あとはAPIに接続するコードに読ませるだけです。
// next.config.js module.exports = { env: { ENVIRONMENT: process.env.ENV } };
// TypeScript (JavaScript) const hostName = () => { switch (process.env.ENVIRONMENT) { case "development": return "https://staging.example.com"; case "production": return "https://example.com"; default: if (process.env.CONNECT === "rails") return "http://localhost:3000"; return "http://localhost:4010"; } }; export const API_HOST = hostName(); // ↑SSGの場合はこの1行がポイントになります。 // クライアントサイドでは process.env が存在しないので、SSG実行時にURLを決定する必要があるためです。
(3) 開発環境だけに認証をかけられる
Netlify の Deploy Preview には、1つ難点があります。それは、プレビュー画面のURLが予測可能な文字列であるということです。。
検索エンジンのクローラーに拾われることはさすがにないと思いますが(とはいえ自前でも robots.txt を置いておきました...)、URLはリポジトリ名やPullReqのIDから決定されるため、認証をかけないと、見せてはいけない情報が第三者に知られてしまう可能性があります。
そこでどうするかですが、このドキュメントにある通り、Netlify にはHTTPレスポンスヘッダをカスタマイズする機能があるため、Basic-Auth を含めて返す設定をして対処しました。
本番環境にもかけてしまうと実際のユーザーが使えなくなってしまうので、Deploy Preview と Branch Deploy 限定でかかるように記述します。
# netlify.toml [context.branch-deploy] command = 'next build && next export && echo -e "/*\n Basic-Auth: $DEV_USERNAME:$DEV_PASSWORD" > out/_headers' [context.deploy-preview] command = 'next build && next export && echo -e "/*\n Basic-Auth: $DEV_USERNAME:$DEV_PASSWORD" > out/_headers' # DEV_USERNAME と DEV_PASSWORD は、Netlify 側に環境変数として登録してあります。
※注意: この手法は2020年12月時点で、Pro
以上のプランでないと使用できません。
メリット3: デプロイの仕組みを作る必要がない
静的ファイルを本番へデプロイする際、どんなことを考えるでしょうか?
- ダウンタイムなし
- 24時間いつでもデプロイ可能
こんな条件を満たす、いわゆる継続的デリバリーを実現できるような仕組みを自前で用意するのは、けっこう大変だと思います。前述の Node.js サーバーと同じで、経験のあるエンジニアがフロントエンドにいなければ、得意なエンジニアにお願いするしかありません。
そんなとき、Netlify(に限らず、静的サイトホスティングサービス全般)を使うメリットは大きいと思います。
Netlify であれば、Git の main ブランチ(どのブランチでも指定可能)の更新に反応して、上記を自動的に実現してくれます!おかげで、デリバリーのことを心配するコストから開放され、アプリケーションロジックの開発に割ける時間が増えます。
最後に
いかがでしたでしょうか!
ご紹介した内容はあくまでいち事例ではあるものの、管理画面をSPAで実装する場合においては、広く言える内容ではないかなと思っております。
(また執筆の機会があれば、OpenAPI を使ったスキーマ駆動開発や、Next.js 内でのコンポーネント設計などの話もできたらよいなと思っています。)
以上、ご参考になれば幸いです!
アドベントカレンダー、明日の記事は
我らがEM・岡村が担当いたします。お楽しみに〜!