Next.js アプリケーションの version skew 問題に向き合う
Photo by Deva Darshan on Unsplash
こんにちは。ウォンテッドリーでフロントエンド組織のリーダーをしている原 (@chloe463) です。 この記事では、数ヶ月前我々のプロダクトで起きていた不具合の原因と解決策について解説します。 大きく2つの問題が起きていましたが、一見すると無関係に見えた2つの不具合が、1つの重要な仕組みに起因するものでした。その発見が個人的にはアハ体験だったため、その発見の衝撃や喜びを共有したいと思います。
2025/07/08 追記
公開後に「Next.js には Version Skew を防ぐ機能がすでに含まれているため、他のところに原因があるのでは?」という指摘を頂きました。ありがとうございます。
自分の手元で新規の Next.js アプリケーションを立てて調べたところ、たしかにご指摘の通りでした。
したがって、多くの場合 Next.js を使ったアプリケーション開発において、Version Skew については気にしなくてもよいかもしれません。また、後半で出てくる自前でブラウザリロードをするという実装もなくてもよさそうです。
この記事で述べている不具合の根本的な原因は、Next.js そのものではなく私たちが使っている環境や実装に依存することが考えられます。
具体的には以下のいずれかの可能性を疑っています。
- 使用している Next.js のバージョンの問題
- Next.js をそのままではなくカスタムサーバーを入れて使用していること
- 他のライブラリとの組み合わせによるもの
- 上記の複合
深堀り検証が足りない状態で記事を執筆・公開してしまったため誤解を招く箇所があります。申し訳ございません。
上記現象については引き続き検証を行い、判明したら別途記事を書こうと考えています。
このブログはそのまま残します。ただし「一定の条件下においてはこういうこともある」という前提を持って読んでいただけると幸いです。
あらためてご指摘ありがとうございました。
2025/07/24 追記
追加の調査で、真の原因が判明したので追加で記事を書きました。Next.js Version Skew 真相解明編
追記ここまで
不具合の概要
今回取り上げる不具合は次のようなものです。
- 突如現れるエラー表示: 候補者詳細画面など、特定の権限を必要とするページでのエラー表示です。ユーザーは正しい権限を持っているにも関わらず、エラーメッセージが表示されてしまいます。しかし、ページをリロードすると何事もなかったかのように正常にデータが表示されます。
- 意図しない言語の切り替わり: 「英語表示の設定で利用していたのに、画面を遷移したら突然日本語表示に戻ってしまった」という報告がありました。この不具合も、リロードで解消される現象でした。
最初これらの不具合は API でのデータ取得時のエラーによって起こっているものと考えました。つまり、候補者情報を取得する API でのエラーや、言語設定取得時のエラーが起こっていないかということです。この仮説が正しいとすると、エラー通知サービスである Honeybadger や監視サービスの Datadog に何らかのエラーが通知されているはずです。しかし、Honeybadger や Datadog を見てもそれらしいデータがありませんでした。 これらの不具合が起きるタイミングもまちまちで、ブラウザリロードによって解消してしまい、再現手順も不明のため解消の糸口を掴むことに大変苦労しました。
デバッグ作業
なかなか解決の糸口が掴めずにいたある日、偶然自分の環境でも上記のエラー表示の不具合が発生しました。このチャンスを逃すまいと、詳細な調査を進めることにしました。
まず、ブラウザリロードで解消することが分かっていたので、エラー表示となっているページをそのままにしつつ、開発者ツールを開き情報を集めることにしました。また、ブラウザの別タブで同じページを開いて正常動作することを確かめました。この不具合が発生してしまうタブ(A)と、正常動作するタブ(B)の2つを行き来しつつ挙動の差分を探りました。
いくつか調査している上で次のことが分かりました。
- 不具合が起きているタブ(A)では、ページを回遊している間、特定のページに進むと必ずエラーになること。
- 一方、正常動作しているタブ(B)では回遊中、タブ(A)でエラーになってしまうページを開いてもエラーにはならないこと。
- タブ(A) で開発者ツールの Network タブを見ても、400系、500系エラーが発生している API リクエストは存在しないこと。
- エラーにはなっていない(200で返ってきている)が、レスポンスが空 {} のリクエストがあること。
最後の空オブジェクトが返ってくる API リクエストが非常に怪しいと感じたため、これを深ぼることにしました。
Next.js の getServerSideProps の挙動
Next.js は React.js を使ったメタフレームワークです。React.js 単体だとセットアップが面倒なところを Next.js が面倒を見てくれています。そのうちの代表的な機能が Server Side Rendering (SSR) です。また、SSR したいページのコンポーネントとセットで getServerSideProps という特別な名前の関数を書いておくと、SSR 時にサーバー側で他サーバーと通信してデータフェッチ等を行うことができます。サーバー側で初期データ取得処理を行うことで、パフォーマンス上のメリットなどがあります。
この getServerSideProps はそのページが SSR されるとき -- つまりフルページリロードが実行されるタイミング -- には HTML 生成処理の前段で実行されます。一方、Next.js アプリケーション上で SPA 的なページ遷移をするタイミングでは WebAPI 的な挙動をします。以下に例を示します。
- ページA (
/a) , B (/b) がありどちらも page コンポーネントで、どちらにもgetServerSidePropsが定義されている。 - ブラウザのアドレスバーに
/aを入れてページA を開く。 - このとき、ページA用のコンポーネントの SSR 処理が実行される。(
getServerSidePropsでデータ取得処理等を行ったあと、関連する React コンポーネントのレンダリングを行って HTML を生成する) - 4.ページA中にあるページBへのリンクをクリックし、ページBを開く。 (もちろん
/bへのリンクは Next.js のLinkコンポーネントを使用している必要がある) - このとき、ページB用の
getServerSidePropsは WebAPI のような動きをして、非同期的に呼び出されページBコンポーネント用の props をレスポンスとして返す
5のときの WebAPI の URL は /_next/data/${ハッシュ値}/${ページURL} というようなものです。
さきほどのデバッグ作業時の状況に話を戻すと、この API の URL に微妙な差があることが分かりました。それは URL にあるハッシュ値と書いた部分です。
- エラーのタブ:
/_next/data/EiOs2OY-K49oARFtpcV-6/app/candidates/....json - 正常なタブ:
/_next/data/nfG5RQ0-jmUXTyPiHS0Tz/app/candidates/....json
これが原因かもしれないと考えさらに調査したところ、真相を掴むことができました。
原因の特定:デプロイを跨いだ buildId の不一致
調査を進めると、さきほどハッシュ値と書いたものは、 Next.js の buildId と呼ばれるものだと行き着くことができました。 さらに、この buildId がブラウザに持っているバンドラに含まれるものと、サーバー側にある Next.js アプリケーションにあるものとで差があると、getServerSideProps で正常にデータ取得ができないことが分かりました。 この buildId は Next.js アプリケーションをビルドする度に決まります。(設定で git commit hash 等や任意の値に書き換えることは可能) しかし、開発環境では buildId は development に固定されてします。そのため、先述した不具合は開発環境では起きないものでした。
つまりまとめると次のようになります。
ユーザーがブラウザでアプリケーションを開いたままにしている間に、サーバー側で新しいバージョンがデプロイされる。その後ユーザーがSPA遷移を行うと、ブラウザは手元に保持している古い buildId を使ってサーバーにデータを要求する。しかし、サーバーは既に新しい buildId に更新されているため、古いIDでのリクエストを不正なものとみなし、データを返さず空のオブジェクトを返却してしまう。
2025/07/08 追記
上記が指摘のあった箇所です。本来 404 を返すべきところなぜか私たちの環境では {} が返却されるようになってしまっていました。本来であれば 404 が返却され Next.js が自動でリフレッシュしるようにできているはずです。
追記ここまで
エラーとなっていたページのコンポーネントでは getServerSideProps から non-null な値が返ってくることを期待していました。getServerSideProps から null など falsy な値が返ってきた場合はエラー画面を表示するという実装になっていたため冒頭で説明したエラーになってしまっていました。下が対象箇所の簡易実装です。
async function getServerSideProps() {
const data = await fetchSomeDataFromServer();
return {
props: { data }
}
}
export default function PageComponent(props) {
// buildId の不一致が起きた時、この分岐に入ってしまう
if (!props.data) {
return <ApiErrorComponent />
}
return (
<AwesomeFeature />
)
}これは権限が必要なページの不具合についての原因にもなりますが、言語のエラーの原因になりうることも分かりました。言語設定も getServerSideProps で取得していましたが、もし getServerSideProps で言語設定が取得できない場合は日本語にフォールバックするように実装してしまっていました。英語で表示していたが、別ページに SPA 遷移したときに getServerSideProps で {} を取得してしまうと、言語設定が日本語設定に勝手に変更されてしまっていました。
async function getServerSideProps() {
const language = await fetchLanguageFromServer();
return {
props: { language }
}
}
export default function PageComponent(props) {
// buildId の不一致が起きた時、 props は `{}` となり、language が ja にフォールバックしてしまう
// ページ遷移前は language は en だったのに、突然 ja になってしまう
const language = props.language || "ja";
return (
<LanguageProvider value={language}>
<AwesomeFeatureComponent />
</LanaugeProvider>
)
}フロントエンドアプリケーションとバックエンドアプリケーションのバージョン違いによって問題が起きてしまうというのはいわゆる Version Skew と呼ばれる問題です。特に強固なスキーマを定義する API -- GraphQL, gRPC など -- を採用していると起きやすい問題です。バックエンドで非互換な API のスキーマ変更を行うとフロントエンドアプリケーションが壊れてしまうという問題です。(例えば、Mutation の input として nullable だったものが non-nullable になるなど)
ウォンテッドリーではマイクロサービスアーキテクチャを採用しているため、複数のマイクロサービス間の Version Skew については通常気をつけながら開発を行っていましたが、一つの Next.js アプリケーション内でも起きる問題ということに目が向いておらず、原因特定まで時間がかかってしまいました。
解決策:バージョンの不一致を検知、同期させる
原因が特定できたので解消のために修正を入れます。原因は先述のとおり、ブラウザが持っているバンドルの buildId とサーバー側にある buildId が異なることです。これを強制的に一致させることができれば解消できそうです。 SPA 遷移をするタイミングで、buildId がサーバー側と一致しているかを判定し、差があればブラウザをリロードしてバンドルを取得し直します。具体的なフローは次のとおりです。
Next.jsの Router は、SPA遷移時に処理をフックできる機構を用意しているため、ここに次の処理を挟みます。 routeChangeStart イベント時に次を実行します。
参考: https://nextjs.org/docs/pages/api-reference/functions/use-router#routerevents
- 現在ブラウザが持っているバンドルの buildId を取得する。
#__NEXT_DATA__という id が付いた DOM の中に書かれているので、少し汚い手段だが、document.querySelectorを使って取得する。
- 取得した
buildIdを使って/_next/static/${buildId}/_buildManifest.jsというパスのファイルが存在するかを確認する。 - このファイルが存在すれば、サーバー側と
buildIdが揃っていると判断する。 - 存在しない場合は、違う buildId のアプリケーションがデプロイされていると判断し、ブラウザをリロードする。
かなりナイーブな手段ですが、これで一旦の解消ができました。
◇ コラム
今回は上記のようなナイーブな手段で解消しましたが、Next.js の設定には deploymentId があるためこれでも解消できるかと思います。
まとめ
Next.js アプリケーションで起きていた不可解なエラーの解消において得られた知見について共有しました。一見無関係に見えた2つの不具合が実は1つの共通の仕組みが原因だったと分かった瞬間は電流が走るような感覚でした。 先述の通り、Version skew 問題は複数のマイクロサービス開発時に気をつけるべきことという認識がありましたが、Next.js アプリケーションという一見一つのアプリケーションと考えられるものでも起きる問題であるというのは -- 冷静に考えるとたしかにそうだが -- 意外な発見でした。 今後同じようなエラーに遭遇した人が自分のように数日消費しないことを望みます。 また今後自分たちのプロダクトも高い品質を維持できるように努力していきます。 ありがとうございました。