クローズしたサービスの管理画面を静的サイトにする

こんにちは、技術部の石川です。

ある日、社内の各種アプリケーションを眺めている中で、とあるクローズしたサービスの管理画面を担っていたウェブアプリが今も動いていると気付きました。簡単にヒアリングしたところ、サービス自体はクローズしたものの、保有していたデータが次のチャレンジに生かせるため管理画面だけ残しているとのことでした。

一方で、その管理画面へのアクセスはそう多くありませんでした。毎日ちょっとだけのリクエストを処理するためだけにデータベースとサーバーが動いており、少し無駄がある状態になっていました。

やや気になったので検討した結果、最終的にこの管理画面アプリを Next.js 製の静的なデータビューワーサイトとしてリニューアルし、社内向けの GitHub Pages として提供されている状態にできました。この記事ではその顛末をご紹介します。

技術選定

いくつか事前調査をした結果、今回の管理画面について以下のことが分かりました。

  • Rails が動いている。サーバー側では graphql-ruby を利用した GraphQL API が動いていて、ウェブフロントエンド側では素の React が API にリクエストを行いながらページを作っている。データベースは PostgreSQL (Amazon RDS for PostgreSQL)。
  • データ量はそこまで多くないが、目で全件確認できるほど少なくもない。
  • ページの種別は、Rails の app/javascript/pages 下にある index.tsx を数えてみると 80 程度。移植しなくて良いページもそれなりにありそう。
  • 画像や映像を表示しているページがある。
  • データの追加や更新はもう行わない。
  • 認証・認可は不要になる。正確には、社内ネットに閉じた環境であれば全公開で構わない。
  • 予定としては今後数年アクセスするつもりがある。
  • データの一覧ページのところにある検索機能は残したい。

この状況下で、現状の管理画面アプリに替わる運用として以下の選択肢を考えました。

  • DWH へのクエリで済ませてしまう。クックパッドでは Amazon Redshift に各種データを集積し、DWH として活用しています。*1今回のアプリが利用しているデータベース上の情報は DWH にもあるため、キレイな画面は消してしまって素朴な SQL クエリにしてしまうことは可能です。クエリ結果を共有するアプリが常用されていたりもします。*2
  • BI ツールのダッシュボードとして再実装する。
  • データベースやアプリをサーバーレスな構成に移植する。
  • 静的サイトジェネレーターの何かしらを使って再実装する。
  • 今ある管理画面に対して古典的なウェブスクレイピングを行って全ページのファイルを取得し、それを手直ししたうえで静的サイトとして提供する。検索機能は諦めて、ブラウザのページ内検索を使う。

このうち、DWH 案とダッシュボード案はすぐに取り下げました。画像や映像とテキストが横に並んだ状態でパッと一覧できる現状を保ちたいという利用者からの要望があったのと、再実装したいページ種別数がそれなりにあってクエリやダッシュボードとして作るには時間がかかりそうだったのが理由です。

サーバーレス化をやってみるのも面白そうではありました。たとえばデータベースだけ Amazon Aurora Serverless にしてアプリはそのまま、というのは社内に過去事例もありできそうでした。一方でアプリが残る以上そのうちセキュリティアップデート等の対応は必要になるため、もっとラクができるなら嬉しいと考えました。クローズしたサービスのアプリはオーナー不在になる確率が高そうで、誰がメンテナンスするのかという問題もありました。*3

というわけで静的サイトジェネレーターかスクレイピングの 2 択になりました。ラクそうだったスクレイピングを選んでも良かったのですが、ここで少し欲を出して、静的サイトジェネレーターを試してみたいという気分になりました。

静的サイトジェネレーターを使う場合、元々の画面が React で実装されているので、移植の容易さを考えると Next.js や Gatsby などの選択肢が考えられます。実は Gatsby で静的サイト化するのは別の小さな社内向けアプリで前例がありました。ただ 2023 年現在の社内では Next.js を利用しているアプリが多くあり、また Next.js の静的サイト向け機能である Static Exports を自分で使ってみたことがなかったため、技術検証の意味も込めて Next.js を使ってみることにしました。*4

React 以外の依存関係も確認しておきましょう。元々の管理画面のコードで使われている package.json を見てみたところ、UI ライブラリとして Ant Design (antd)、GraphQL クライアントとして Apollo が入っており、その他小さなライブラリがすこし入っているという状況でした。このくらいの複雑度なら移植できそうだなと判断しました。

さて、Next.js の Static Exports で再実装するのであれば、PostgreSQL に入っているデータをどうするか考えなければいけません。AWS 上にデータベースが残ったままだとコストはあまり減りませんし、ローカル環境のデータベースに移すならシンプルな形にしたいです。考えた結果、今回のデータはそこまで巨大では無かったため、SQLite へ移植して .sqlite3 ファイルとして持ってしまうことにしました。jsonb 型など PostgreSQL 固有の機能を使っている箇所もあったのですが数箇所しか無かったため SQLite 向けに書き直し、SQLite のみだと足りない処理はデータベースからデータを取ってきた後にアプリ側で行うようにしてしまいました。

データ取得まわりについては GraphQL を使うことにしました。元々の Rails 製 GraphQL API をコピペしてくれば GraphQL スキーマとサーバーが出来ますし、クライアント側でも型が自動生成できてラクが出来そうという目論見がありました。SQLite に対して直接 SQL クエリを走らせても良かったのですが、自分が元の管理画面の実装にそこまで詳しくなかったというのもあり、どこまで複雑なクエリが必要になるのか実装前の時点で判断しづらかったためコピペにしてしまいました。

実装

方針が決まってしまえば後は実装するだけです。API サーバーは元の Rails のコードをそのままコピペしてきて動かすようにし、フロントエンドは create-next-app しました。Next.js 13 で App Router を使いつつ、Static Exports のために next.config.js が

const nextConfig = {
  output: 'export',
}
module.exports = nextConfig

になっています。*5

next build をする際には横で SQLite に繋がった Rails 製 GraphQL API を動かしていて、必要に応じて API にリクエストすることでデータを取ってきます。Static Exports の場合はビルドが終わると HTML 等のファイル群が生成されるので、開発環境では serve を使うなどすれば閲覧できます。

実装の移植について、ページや React Components の移植はまあまあコピペで終わりました。元々の実装がそれなりにコンポーネント化されていて全容を把握しやすかったのと、元と変わらず GraphQL を使っているあたりが効きました。

ただし Server Components と Client Components、つまりどのコンポーネントの処理はサーバー側で行われどの処理はブラウザ側で行われるのかについては整理する必要がありました。Static Exports を行ううえでは API からデータを取得する部分はすべてサーバー側で行われていないといけませんし、逆に useState を使うような箇所はブラウザ側で行われなければなりません。元々の管理画面ではブラウザから GraphQL リクエストを行っていたため、至るところにある API リクエストはサーバー側に集中するよう書き換えが必要でした。データの流れを整理した結果としてひとつのコンポーネントを分割して Server Component と Client Component に分けたりもしました。

とはいえ「データの追加や更新はもう行わない」という制約がとても強く効いていて、実装は比較的シンプルになりました。データの追加・更新のために存在していた React コードをばっさり削除していった結果、大抵のページは「Server Components でデータを取得して、子となる UI コンポーネントにデータを渡す。子コンポーネントは必要であれば Client Components にする」くらいの単純さになりました。

Client Components を用いたのは主に、検索フォームを設置しているところと antd 5.8 が必要とするところです。*6

検索機能については、元の管理画面ではサーバー側に検索機能を実装していましたが静的サイトでは不可能なので、ブラウザ側の JavaScript で素朴に String.prototype.includes を使って絞り込むことで実現しました。ページネーションして見た目上の表示件数を減らしはしましたが、それなりの数のデータがあっても高速に動作するのでブラウザは凄いですね……。もし複雑な全文検索が欲しくなる箇所があれば Lunr.js 等を使ってみるつもりでしたが、今回はそこまで複雑な検索は無かったため使わずに済んでしまいました。

そんなこんなで詳細が固まり、まずは複雑そうなページから実装してみたところ上手く動いたため実装を進め、必要なページすべてについて実装しきることができました。

振り返り

全部終わったあと振り返ってみると、Next.js の App Router と GraphQL を使うことにしたのは成功だったと感じます。コピペできたのもそうですし、実装している最中、App Router のディレクトリ構造を使って GraphQL クエリや小さい React Components のファイルたちをページの実装の近くに配置できるのがラクでした。

具体的にはたとえば graphql-codegen で near-operation-file を使うようにしたうえで、以下のようなファイル配置になるわけです。

app
├── _lib
│   └── types.generated.ts
├── recipes
│   ├── _lib
│   │   ├── Table.tsx
│   │   ├── query.generated.ts
│   │   └── query.graphql
│   └── page.tsx
:
:

元々の実装もページごとのコードとページ固有の React Components、それとたまに全体で共有の React Components という感じだったので、それをそのまま持ってくることが出来ました。データの流れを整理する過程で小さい Server Components や Client Components が生まれたのですが、このディレクトリ構造だと気になりません。

Static Exports は何の問題もなく動いてくれました。移植の際に <a> タグをすべて next/link の <Link> に書き換えたため、ただでさえ静的サイトで速いのに prefetch のおかげで更に速く感じられるサイトが出来上がりました。

いちおう実装前の不安点として、antd などの依存ライブラリは実装時点での最新バージョンまで上げないと App Router および React Server Components 対応の不充分な点がありそうだとは分かっていました。この関係で元の管理画面で使っていたバージョンからメジャーバージョンを上げないといけないライブラリもありました。とはいえ実装前にザーッと各ライブラリの変更履歴を眺めた結果そこまで困らなさそうと判断し、実際あまり困りませんでした。

また App Router の関係なのか Static Exports の関係なのかはちゃんと調べていませんが、いくつかのエラーで原因を調べるのに少し時間がかかりました。エラーメッセージとスタックトレースが分かりづらかったのですよね……。"use client"; をつけて Client Components にしたら直るのだけどエラーメッセージから直接は分かりづらい類のエラーにはいくつか遭遇しました。

とはいえ始まりから終わりまで見ると、そう労力をかけずに移植できたので満足です。この管理画面が提供しているデータは社内を見渡してもそれなりにユニークな料理データでして、これを参照しやすい形で残し続けられることには価値があると、個人的にも考えています。そういった意義のついでに技術検証も出来たオトクな仕事でした。

*1:https://techlife.cookpad.com/entry/2019/10/18/090000

*2:https://techlife.cookpad.com/entry/2021/06/11/120000

*3:もちろん静的サイトにしたとしても JavaScript ライブラリの更新が必要になる可能性が無いとは言えないのですが。数年くらい経って新しいブラウザーでうまく動かなくなったときくらいでしょうかね。

*4:この管理画面を開発していたエンジニアはサービスクローズ後も社内に在籍しているのですが、このあたりの技術検証をしてみたいという自分の要望から自分が実装してみることにしたのでした。

*5:最終的な next.config.js では更に、next/image の最適化は Static Exports では意味が無いので unoptimized: true にしたり、実装の都合で trailingSlash: true にしたりもしています。GitHub Pages にデプロイする前には basePath を調整するのもやっています。

*6:antd で必要になるのは https://ant.design/docs/react/use-with-next#using-nextjs-app-router に書かれている "if you use the above sub-components in your page, you can add "use client" to the first line of the page component to avoid warnings" です。