6万行規模の生成AI SaaSをNext.js 15にアップデートした話
はじめに
ストリーツ株式会社の@hanamaです。
弊社では、生成AIを活用したメディア向けコンテンツ制作支援SaaSである「apnea」を開発・運営しています。
今回は、先月に行ったapneaのNext.js 15へのアップデートについてご紹介します。本プロダクトは、Next.jsを用いたフルスタックアプリケーションであり、ts
、tsx
ファイルのみで6万行(論理LOC)を超えるコードベースを有しています。
執筆時点ではこの規模のNext.jsで構築されたアプリケーションのバージョン15へのアップデートに関する事例がネット上で見つけられなかったため、今回はその手順と課題について詳しくお伝えすることで、同様のアップデートを検討している方々の参考になればと思います。
本記事はNext.js Advent Calendar 2024 の12月3日の記事です。
要点まとめ
本記事はアップデート時に遭遇した問題とその対策を実際の作業の時系列に沿って記載しています。長い記事になってしまったため、以下に要点をまとめました。
- Next.js 15は、React 19RCに依存しているため、多くのライブラリで
peerDependencies
のバージョン不整合によるエラーが発生します。-
npm
の場合、--legacy-peer-deps
オプションを付与することでアップデートできます。 -
pnpm
やyarn
を利用している場合は、warning
として表示され、アップデート自体には成功することが多いです。
依存確認のスクリプト
check-react-deps.jsconst { execSync } = require("child_process"); const fs = require("fs"); const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; Object.entries(dependencies).forEach(([pkg, currentVersion]) => { try { // 現在のバージョンに関するpeerDependenciesを取得 const currentPeerDepsOutput = execSync(`npm info ${pkg}@${currentVersion} peerDependencies --json`, { encoding: "utf-8" }); const currentPeerDeps = JSON.parse(currentPeerDepsOutput || "{}"); const currentReactSupport = currentPeerDeps.react || "No peerDependencies for React"; // 最新バージョンに関するpeerDependenciesを取得 const latestPeerDepsOutput = execSync(`npm info ${pkg} peerDependencies --json`, { encoding: "utf-8" }); const latestPeerDeps = JSON.parse(latestPeerDepsOutput || "{}"); const latestReactSupport = latestPeerDeps.react || "No peerDependencies for React"; // 最新バージョン情報を取得 const latestVersionOutput = execSync(`npm info ${pkg} version`, { encoding: "utf-8" }); const latestVersion = latestVersionOutput.trim(); // Reactへの依存があるもののみについて、結果を出力 if (currentReactSupport !== "No peerDependencies for React" || latestReactSupport !== "No peerDependencies for React") { console.log(`${pkg}@${currentVersion} supports React version: ${currentReactSupport}`); console.log(`The latest version ${pkg}@${latestVersion} supports React version: ${latestReactSupport}\n`); } } catch (err) { console.error(`Error checking ${pkg}@${currentVersion}:`, err.message); } });
-
- よく使われていそうなライブラリで、React 19に対応していないものについてはこちらのセクションを参照ください。
調査したライブラリ
@stripe/react-stripe-js
next-auth
shadcn/ui
react-day-picker
@tiptap
-
codemod
のnext-async-request-api
のエッジケースについて-
params
を引数として取るものの、コンポーネント内で利用していないページコンポーネントについては、自動変換が行われずnext build
に失敗することがあるため手動での修正が必要です。
-
Next.js 15について
Next.js v15.0.0は、2024年10月にリリースされたNext.jsのメジャーバージョンです。現在の最新バージョンはv15.0.3となっています。
15系へのアップデートでの変更点などは、公式ドキュメントや公式ブログをご参照ください。
アップデート前の状況
アップデート前の状態は以下の通りです。
- Node.js: 20.10.0
- npm: 10.2.3
- Next.js: 14.2.15
- App Routerを採用
- React: 18.3.1
- 依存ライブラリ
-
dependencies
: 約130個のパッケージ -
devDependencies
: 約40個のパッケージ - 最新バージョンでないものも含まれる
-
- コードベース
-
ts
ファイル数: 約250ファイル -
tsx
ファイル数: 約500ファイル -
ts
、tsx
ファイルのみで6万行(論理LOC)を超える
-
アップデート作業手順
ここからは実際にどのような手順でNext.js 15へのアップデートを試みたかについて順を追って記載していきます。
1. 公式ドキュメント通りに自動アップデートを試す
とりあえず、公式ドキュメントに記載の以下のコマンドを実行してみました。
npx @next/codemod@canary upgrade latest
対話式にいくつかの質問が表示されます。TurboPack
はまだ利用しないため、適用せずcodemod
の実行とReact 19へのアップデートを選択しました。
React 18のままでもNext.js 15へのアップデートが可能では?と思った方へ
Next.js 15では、引き続きReact 18の利用も可能ですが、Pages Routerのための後方互換性のためであり、App Routerを利用する場合にはReact 19が必要です。
以下公式ブログからの引用です。
In version 15, the App Router uses React 19 RC, and we've also introduced backwards compatibility for React 18 with the Pages Router based on community feedback. If you're using the Pages Router, this allows you to upgrade to React 19 when ready.
アップデートに失敗し、エラーが表示されました。依存ライブラリのpeerDependencies
がNext 15やReact 19に対応していないものがあるようです。
大量のエラーが表示されたため、このままではアップデートが難しいと判断しました。
今後の方針
- React 19に対応していない依存ライブラリを洗い出して対応
- React 19へのアップデートを行う
- Next.js 15へのアップデートを再度実行
2. React 19へのアップデートの準備
まずは利用している依存ライブラリがReact 19に対応しているかを調査しました。
package.json
に記載の依存ライブラリに対して、npm
リポジトリの情報からReact 19への対応状況を調査すれば良いのですが、手作業で行うのは大変なので簡単なスクリプトを書いて調査しました。ステップ1では、直接dependencies
で依存しているライブラリに関するエラーは表示されなかったため、peerDependencies
に関する情報を取得しました。
スクリプトの内容
const { execSync } = require("child_process");
const fs = require("fs");
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
Object.entries(dependencies).forEach(([pkg, currentVersion]) => {
try {
// 現在のバージョンに関するpeerDependenciesを取得
const currentPeerDepsOutput = execSync(`npm info ${pkg}@${currentVersion} peerDependencies --json`, { encoding: "utf-8" });
const currentPeerDeps = JSON.parse(currentPeerDepsOutput || "{}");
const currentReactSupport = currentPeerDeps.react || "No peerDependencies for React";
// 最新バージョンに関するpeerDependenciesを取得
const latestPeerDepsOutput = execSync(`npm info ${pkg} peerDependencies --json`, { encoding: "utf-8" });
const latestPeerDeps = JSON.parse(latestPeerDepsOutput || "{}");
const latestReactSupport = latestPeerDeps.react || "No peerDependencies for React";
// 最新バージョン情報を取得
const latestVersionOutput = execSync(`npm info ${pkg} version`, { encoding: "utf-8" });
const latestVersion = latestVersionOutput.trim();
// Reactへの依存があるもののみについて、結果を出力
if (currentReactSupport !== "No peerDependencies for React" || latestReactSupport !== "No peerDependencies for React") {
console.log(`${pkg}@${currentVersion} supports React version: ${currentReactSupport}`);
console.log(`The latest version ${pkg}@${latestVersion} supports React version: ${latestReactSupport}\n`);
}
} catch (err) {
console.error(`Error checking ${pkg}@${currentVersion}:`, err.message);
}
});
このスクリプトを実行した結果の出力例は以下の通りです。
現在のバージョンで対応しているReactのバージョンと、最新バージョンで対応しているReactのバージョンが表示されます。
@radix-ui/react-icons@1.3.0 supports React version: ^16.x || ^17.x || ^18.x
The latest version @radix-ui/react-icons@1.3.2 supports React version: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
この結果を元に各依存ライブラリを以下のように分類した上で、React 19へのアップデートを進めることにしました。
分類 | アプデリスク | 対応方針 |
---|---|---|
React 19 RCに対応済み | 低 | 特に対応しない |
React 19に対応済み | 低 | 特に対応しない ( --legacy-peer-deps オプション付与) |
今のバージョンではReact 19に対応していないが、最新版では対応済み | 中 | 最新版にアップデート 破壊的変更が行われている可能性があるため、Change Logを要確認 |
最新版でもReact 19に対応していない | 高 | Githubなどでどのように対応されるかを確認する |
よく使われていそうなライブラリで、特に対応が必要そうなもの
このセクションでは、他のNext.js プロダクトでもよく使いそうなライブラリのうちReact 19RCに対応していないものをピックアップし、リポジトリなどから抽出した対応方針・アップデートにおいての注意点を記載します。
-
@stripe/react-stripe-js
- React 19への対応を求めるissueにて会話されているが、RC版であることから対応はまだ行わない方針のようです。
以下コメントより引用。React 19 is still RC and not a stable release, so for now it's not supported/tested. Once that's release I expect we'll work towards updating that to allow 19.
- メンテナによればReact 19RCにおいても確認されている問題はないとのことなので、
--legacy-peer-deps
オプションを付与してアップデートを行うことにしました。
以下コメントより引用Currently, nothing is not working as expected.
- React 19への対応を求めるissueにて会話されているが、RC版であることから対応はまだ行わない方針のようです。
- next-auth
-
shadcn/ui
-
公式ドキュメントにて、Next.js 15 + React 19で利用するための手順や留意事項が記載されています。
shadcn/uiで利用しているライブラリの対応状況 - 特筆すべきはreact-day-pickerの対応状況です。
-
shadcn/ui
では現在react-day-picker
のv8系を利用していますが、v8系のpeerDependencies
には^16.8.0 || ^17.0.0 || ^18.0.0
と記載されており、公式にはReact 19への対応が明記されていません。
shadcn/ui
のドキュメントでは、--legacy-peer-deps
オプションを付与してアップデートを行うことで動作すると記載があり、v9系への対応も進められているようです。 - なお、弊社ではexpansionであるdatetime-pickerを利用していました。このexpansionでは
react-day-picker
のv9系への対応が先んじて行われていたため、弊社ではreact-day-picker
のv9系へのアップデートを行いました。
手動で対応したい方はreact-day-picker
のUpgrading to v9を参考にしてください。
-
-
公式ドキュメントにて、Next.js 15 + React 19で利用するための手順や留意事項が記載されています。
-
@tiptapのextension
- @tiptap/react本体はReact 19に対応しているがextensionの中にはReact 19に対応していないものが残っています。
- 特にPro Extensionに関してはリポジトリが公開されていないため、対応状態が外から見て確認できません。
-
node_modules
から対象のパッケージのpackage.json
とindex.esm.js
を確認したところ、弊社で採用していたプラグインに関しては、React 19で行われた変更の影響を受けなかったため、--legacy-peer-deps
オプションを付与してアップデートを行うことにしました。
-
- また、tiptap上で
Tooltip
などを作る際に使うtippy.jsが、React 19に対応しないまま2024年11月9日にリポジトリがアーカイブされています。- 将来的に削除されるためReact 19から非推奨とされている、
element.ref
へのアクセスを行っている部分が存在するため動作には問題がないものの、常に警告が表示される状態になっています。 - tiptapチームでは、代替のFloating UIへの移行を進めているようですが、それまでに
tippy.js
を利用している部分については僕が作成したフォークリポジトリの変更点を参考に変更してみて下さい。
- 将来的に削除されるためReact 19から非推奨とされている、
3. React 19へのアップデート
ここまでくると、安心して--legacy-peer-deps
オプションを付与したReact 19へのアップデートを行うことができます。
npm install --save-exact react@rc react-dom@rc --legacy-peer-deps # --legacy-peer-depsオプションをつける
型定義もアップデートしておきましょう。
{
"devDependencies": {
-- "@types/react": "18.3.11",
++ "@types/react": "npm:types-react@rc",
-- "@types/react-dom": "18.3.0",
++ "@types/react-dom": "npm:types-react-dom@rc"
},
}
codemod
でReact 19のBreaking Changesに対応する
npx codemod@latest react/19/migration-recipe
全てのcodemodを適用します。
? Press Enter to run the selected codemods in order. You can deselect
anything you don’t want. (Press <space> to select, <a> to toggle all, <i>
to invert selection, and <enter> to proceed)
❯◉ react/19/replace-reactdom-render
◉ react/19/replace-string-ref
◉ react/19/replace-act-import
◉ react/19/replace-use-form-state
◉ react/prop-types-typescript
4. Next.js 15へのアップデート
React 19へのアップデートが完了したので、次はNext.js 15への対応状況を確認します。
Reactと同様に以下のスクリプトを実行し、依存ライブラリがNext.js 15に対応しているかを調査しました。
スクリプトの内容
const { execSync } = require("child_process");
const fs = require("fs");
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
Object.entries(dependencies).forEach(([pkg, currentVersion]) => {
try {
// 現在のバージョンに関するpeerDependenciesを取得
const currentPeerDepsOutput = execSync(`npm info ${pkg}@${currentVersion} peerDependencies --json`, { encoding: "utf-8" });
const currentPeerDeps = JSON.parse(currentPeerDepsOutput || "{}");
const currentNextJsSupport = currentPeerDeps.next || "No peerDependencies for Next.js";
// 最新バージョンに関するpeerDependenciesを取得
const latestPeerDepsOutput = execSync(`npm info ${pkg} peerDependencies --json`, { encoding: "utf-8" });
const latestPeerDeps = JSON.parse(latestPeerDepsOutput || "{}");
const latestNextJsSupport = latestPeerDeps.next || "No peerDependencies for Next.js";
// 最新バージョン情報を取得
const latestVersionOutput = execSync(`npm info ${pkg} version`, { encoding: "utf-8" });
const latestVersion = latestVersionOutput.trim();
// 結果を出力
if (currentNextJsSupport !== "No peerDependencies for Next.js" || latestNextJsSupport !== "No peerDependencies for Next.js") {
console.log(`${pkg}@${currentVersion} supports Next.js version: ${currentNextJsSupport}`);
console.log(`The latest version ${pkg}@${latestVersion} supports Next.js version: ${latestNextJsSupport}\n`);
}
} catch (err) {
console.error(`Error checking ${pkg}@${currentVersion}:`, err.message);
}
});
このスクリプトを実行した結果の出力例は以下の通りです。
next-auth@4.24.10 supports Next.js version: ^12.2.5 || ^13 || ^14 || ^15
The latest version next-auth@4.24.10 supports Next.js version: ^12.2.5 || ^13 || ^14 || ^15
弊社のプロジェクトではNext.js 15に対応していないライブラリは見つからなかったため、そのままアップデートを行うことにしました。
--legacy-peer-deps
オプションを付与してアップデートを行うため、以下のコマンドでライブラリのみをまずアップデートします。
npm install next@latest eslint-config-next@latest --legacy-peer-deps
Next 15のBreaking Changeに対応する
codemod
で自動的に書き換え
npx @next/codemod@latest next-async-request-api .
npx @next/codemod@latest app-dir-runtime-config-experimental-edge .
弊社のプロダクトはCloud Runにデプロイしており、エッジコンピューティングやVercelへのデプロイは行なっていないため、app-dir-runtime-config-experimental-edge
やnext-request-geo-ip
は実行していません。
codemod
で自動的に書き換えされないエッジケース
一連の対応を行なった後、以下のエラーでnext build
に失敗しました。
src/app/xxx/page.tsx
Type error: Type 'XXXProps' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ id: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]
一部のページコンポーネントについて、params
がPromise
でラップされていないというエラーです。
これはparams
を引数として取るものの、コンポーネント内で利用していないページコンポーネントに関しては、codemod
で自動変換が行われないことが原因です。params
を削除するかPromise
でラップするように変更しましょう。
ESLintのno-unused-vars
ルールを利用すると、このように不必要なparams
が存在しているとエラーが発生するため、このエラーを未然に防ぐことができます。
再現リポジトリ
この仕様が実装されたPR
キャッシュ周りの変更を確認
-
fetch
関数がデフォルトでキャッシュを利用しなくなりました。 -
Route Handlers
もキャッシュを利用しないようになりました。
弊社のプロダクトではServer Actions主体でバックエンドとのやり取りを行なっているので、fetch
関数がそもそも少なくRoute Handlers
も利用していないため、特に変更は必要ありませんでした。
Client-side Router Cache
への対応
クライアント側でのキャッシュもデフォルトでoff
に変更されました。
ユーザー側のページ遷移のUXを保つため、Next 14のときの設定と同じ設定値にしました。
++ experimental: {
++ staleTimes: {
++ dynamic: 0,
++ static: 300,
++ }
++ },
これにて、Next.js 15へのアップデート作業は完了です。
まとめ
今回は、弊社のプロダクトをNext.js 15にアップデートする際の手順、困ったポイントなどをまとめてみました。
同様のアップデートを予定されている方の参考になれば幸いです。
Discussion