Next.js を v3 から v13 に一気にアップデートした話
2023年夏休みの自由研究がてら、約6年メンテナンスされていなかった Next.js で作られたサイトの開発環境をアップデートしました。Next.js に関しては、なんと v3 から v13 に一気にアップデートでした。
ただやっただけだともったい無いので、せっかくなので記事に残そうと思います。同じように Next.js で作られたサイトのメンテナンスを怠っている人に届いて、何か少しでも助けになれれば幸いです。
背景
まず、アップデートをしようと思った最初のきっかけは、M1/M2 Mac で Node.js を使って普通に開発ができないことでした。6年間メンテされてなかったこともあり、この環境の Node.js のバージョンは v8.4.0
です。(当時はもちろん最新でした😂)
Node.js v16
未満のバージョンを M1/M2 Mac にインストールして使うには、以下の記事のように Rosetta を使用する必要があります。
まあ、このやり方でも開発はできるのですが、せっかく Apple Silicon の Mac を使っているのに Intel Mac 用アプリ向けの Rosetta はできるだけ使いたくないなという気持ちがありました。
もうひとつの理由はセキュリティです。古い Node.js を使っているので、使用しているライブラリも古いものが多く、その分脆弱性もかなりあり、セキュリティリスクが高い状態になっていました。
npm audit
もしくは yarn audit
でインストールされているライブラリの脆弱性を調査することができます。以下はアップデート前の結果です。510
の脆弱性があり、High
のものが 233
、Critical
は 59
もあり、かなりやばい状態です。
まあ、あとは古い Next.js を触るモチベーションも湧かないなど、いろんな理由もあり、Node.js やライブラリをアップデートし、セキュリティリスクも限りなく 0 に近い状態を目指すことにしました。
以下は作業の記録です。
(パッケージ管理には npm
を使用します。)
ちなみに
6年前というと、僕が React Static というフレームワークを使って、初めて React でサイトを作った時期です。以前 jQueryを卒業したかった僕がReact StaticでReactをイチから学んでWebサイトを作った話 という記事も書きましたが、今はもう React Static はメンテナンスモードに入っております😇
それに比べて、というわけでもないですが、Next.js は今でもしっかりアップデートされ続けていて、安心感持てますね。
作業手順
0. ChatGPT に聞いてみた
まずは、ワンチャン何か裏技みたいな方法がないか、ChatGPT に聞いてみました。
ですよね。僕もそれが正しいやり方だと思ってました。
しかし、v3
から v13
までのリリースノートをすべてチェックして、段階的にアップデートするのはさすがに時間がかかります。できれば夏休み(お盆休み)の間に終わらせたいと考えていたので、これは逆に最終手段にして、もう作り直す覚悟で一気に最新バージョンに上げようと決めました。
幸い、このサイトは15ページ程度の小規模なサイトだったので、作り直すという選択肢も現実的です。大規模サイトだったらこうはいかないと思うので、やはりこまめにアップデートしなきゃですねぇ。
1. Node.js をアップデート
まずは、Node.js が古いままだと Rosetta を使わないといけないので、Node.js を v8.4.0
から、最新の LTS の v18.17.1
にアップデートしました。(2023/08/14 現在)
asdf の場合は .tool-versions
、nodenv の場合は .node-version
ファイルを更新します。
nodejs 18.17.1
その後、該当の Node.js を上記ツールなどを使ってインストールします。
2. Next.js や React など主要なライブラリを最新にアップデート
もちろん、これだけだと npm install
は成功しません。インストールされているライブラリに 古い fsevents が使われていて、それがインストールした Node.js に対応しておらず node-gyp
周りのエラーが出ます。
このエラーを解消すべく、以下の主要なライブラリをすべて最新版にアップデートしました。
-
Next.js:
v3.2.1
->v13.4.13
-
React:
v15.6.1
->v18.2.0
-
React DOM:
v15.6.1
->v18.2.0
-
styled-components:
v2.1.2
->v6.0.7
-
Storybook:
v3.2.6
->v7.2.3
いや、このバージョンの変化を見ると、もう別物と言ってもいいですね。
他にもいくつもライブラリは入っていましたが、必要のないライブラリはできるだけ削除していきたいと考えていたので、まずは絶対に必要な分だけアップデートしています。
上記のアップデートをすることで、まずは npm install
が成功する状態になりました。
3. ディレクトリ構成を変更
僕はあまり以前の Next.js のことは詳しくないのですが、今とディレクトリ構成が違っていたのかな?今は開発用ファイルは このプロジェクトではルートディレクトリに src
ディレクトリ、静的ファイルは public
ディレクトリという構成に基本なっていると思いますが、components
や pages
などが置かれている構成になってました。
僕自身 src
ディレクトリで開発することに慣れていることもあり、ルートにすべてあると見通しが悪かったので、開発ファイルは全て src
ディレクトリに移しました。
また、このプロジェクト特有なのかわかりませんが、静的ファイル用のディレクトリが static
になっていたので、Next.js の静的ファイル配信のルールに従って public
ディレクトリに変更しました。
この時 /static/images/...
のようになっているパスを /images/...
に変更します。
以下のような、Next.js のディレクトリ構成になっています。
/
|- /.next
|- /node_modules
|- /out
|- /public
|- /src
| |- /components
| |- /containers
| |- /pages
|- .gitignore
|- .eslintrc
|- next.config.js
|- package-lock.json
|- package.json
|- README.md
追記:2023/08/19
はてなブックマーク で「src ディレクトリがあるのが基本だっけ?」というコメントをいただきました。僕もそこまで深く調査をせずに書いてしまったので、コメントとてもありがたいです。
おっしゃる通り create-next-app --src-dir
を使うことで src
ディレクトリを使うようになるので、その点では src
ディレクトリを使わない方がデフォルトとも言えますが、npx create-next-app
を実際に叩くと、Next.js プロジェクトのセットアップのためのプロンプトが表示されますが、その中の Would you like to use src/ directory?
の質問では YES
がデフォルトで選ばれているので、その点だと src
ディレクトリを使う方がデフォルトと言えるのかもしれません。
なので、今のところ src
ディレクトリあり/なしのどちらが基本的なディレクトリ構成だとも言いづらいので、上の文章は打ち消し線を入れさせてもらいました。僕の勝手な解釈で書いてしまうと、見た方が勘違いしてしまう可能性があるので、本当気をつけないとですね。改めて、コメントありがとうございました!
4. next.config.js を書き換え
準備は整ったので、次は next
コマンドで開発サーバーが起動するところを目指します。
next.config.js
に関しては調べたところ、そこまで大きく書き方も変わっておらず、アップデートに苦労しませんでした。元々そんなに config ががっつり書かれていなかったので、助かりました。
変更したのは以下の2点です。
-
assetPrefix
->basePath
に-
assetPrefix は今の Next.js のバージョンでも使えそうですが、basePath というオプションが
v9.5
から追加されていました。今回のプロジェクトの場合はまさに/doc
のようなサブパスでのサイトでしたので、basePath
を使うよう変更します。
-
assetPrefix は今の Next.js のバージョンでも使えそうですが、basePath というオプションが
basePath: '/xxx',
- compiler option に styled-components を指定
このプロジェクトでは styled-components を使っているので、compiler オプションに styled-component を指定します。
compiler: {
styledComponents: true,
},
以前は babel-plugin-styled-components
を使ってコンパイルしていましたが、今の Next.js では babel-plugin-styled-components
が移植されており、このオプションを設定するだけでコンパイルしてくれます。ありがてぇ。
We're working to port babel-plugin-styled-components to the Next.js Compiler.
First, update to the latest version of Next.js: npm install next@latest. Then, update your next.config.js file:
5. babel を削除
先ほど babel-plugin-styled-components
が必要なくなったので削除しましたが、Next.js も v12 からデフォルトで Rust製の SWC を使うようになっており、Babel は必要ありません。また、それ以外の Babel 関係の設定(.babelrc
)やプラグインも必要なさそうだったので、一旦削除して、もし必要があればまたインストールするというやり方でいきました。
babel-plugin-module-resolver という import
のパスにエイリアスを使うためのプラグインが入っていたので、削除にあたり一度パスをすべて相対パスに置換しました。
エイリアスの機能は Next.js だと jsconfig.json
もしくは tsconfig.json
の paths
オプションを使って設定できるようサポートされているので、後ほど TypeScript を導入後に設定しています。
6. Flow の型付けを削除
このプロジェクトでは、静的型付けに Flow を使っていました。もちろん Flow は現在もメンテされていますが、僕自身が Flow に詳しくなく、TypeScript の方が書き慣れているのもあり、TypeScript を使うことにしました。
なので、まずは .flowconfig
や flow-typed
ディレクトリを削除し、コンポーネントなどの js ファイルに書かれていた型付けを一旦コメントアウトしました。また、js ファイルの先頭に書かれた // @flow
も全部削除しました。
型付けをコメントアウトしたのは、後で TypeScript を導入した時に、そのコンポーネントがどういう型を持っているかをパッとわかりやすくしたかったからです。実際そのおかげでかなり効率よく導入できたと思います。
// type IProps = {
// text?: string
// }
この場合、型が string の text
という Props があるのがパッとわかりますから、あとは TypeScript の書き方で書き換えるだけという作業になります。
この作業が終わったら flow-typed
などの Flow 関係のライブラリも削除します。
7. styled-components 周りのエラー解消
styled-components も v2
から v6
に上げているので、かなり変更点があると思っていましたが、基本的に変更する必要があったのは v4 で変更された分でした。
-
injectGlobal
->createGlobalStyle
に置き換え
グローバルな CSS を書くための関数が injectGlobal
から createGlobalStyle
に変更されました。以下のような書き方に変更します。
import { injectGlobal } from 'styled-components';
export default injectGlobal`
...
`
// を以下に変更
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
...
`
この GlobalStyles
はコンポーネント化されるので、App
コンポーネントなどで読み込む必要があります。
import GlobalStyles from "../components/GlobalStyles";
const App = ({ children }: IProps) => (
<ThemeProvider theme={theme}>
<GlobalStyles />
{children}
</ThemeProvider>
);
-
.extend
をstyled()
に置き換え
コンポーネントのスタイルを拡張するための .extend
が v4 で廃止されたので、代わりに styled()
を使う書き方に変更します。
const StyledHoge = Hoge.extend`
...
`;
// を以下に変更
const StyledHoge = styled(Hoge)`
...
`;
-
.withComponent
->as
prop に置き換え
同じく .withComponent
という、コンポーネントを別のタグで使用するための機能が廃止され、代わりに as
という Props が追加されたので、そちらを使うよう変更します。
const StyledTableColHeader = StyledTableCol.withComponent('th').extend`
...
`;
// を以下に変更
const StyledTableColHeader = styled(StyledTableCol)`
...
`;
<StyledTableColHeader as="th">...</StyledTableColHeader>
pages/_document.js
のエラー解消
8. Next.js v3
の時になかったのかどうかまでは調べていませんが、_document.js
で Missing component: <Html />
という Warning が出ているので、_document.js
の中の HTML の html
タグを Next.js の Html
コンポーネントを使うように変更します。
Html
コンポーネントを使わないと、正しくレンダリングされなくなる可能性があるので、よほどの理由のない限りは Html
コンポーネントを使った方が良さそうです。
next/link
の passHref
を使わない書き方に変更
9. Next.js v13
から next/link
の passHref
属性を使った書き方では <a>
タグが入れ子になってしまい Hydration エラーが出るようになっています。
なので、passHref
属性を使っている Link
を passHref
を使わない書き方に変更します。
const StyledLink = styled.a`
...
`
...
<Link href="/hoge/" passHref>
<StyledLink>{children}</StyledLink>
</Link>
// を以下に変更
const StyledLink = styled(Link)`
...
`
...
<StyledLink href="/hoge/">{children}</StyledLink>
この時点で、まだコンソールにエラーは出ていたり、崩れはありますが、next
コマンドでひとまず開発サーバーが立ち上がり http://localhost:3000/
でサイトが表示されるようになりました🎉
10. recompose を React Hooks に置き換え
このサイトが作られたときはまだ React Hooks がなかったので、HoC を使ってロジックを書くのが主流でした。recompose は HoC を扱うためのライブラリです。
今は recompose の作者の方も Hooks を使うことを推奨していますので、recompose を使って HoC で書いている部分を React Hooks に置き換えます。
今回のプロジェクトではそこまでがっつり HoC で書かれている部分はなかったので、そこまで苦労しませんでした。例えば next/router
の withRouter()
と recompose
を使っていた部分は以下のように置き換えました。
import { compose } from 'recompose';
import { withRouter } from 'next/router';
...
export default compose(
withRouter,
)(Hoge);
// を以下に変更
import { useRouter } from "next/router";
...
const Hoge = () => {
const router = useRouter();
...
}
export default Hoge;
僕もそうですが、そこまで recompose に詳しくない人にとっては、複雑なロジックの置き換えは大変だと思います。こういう時は ChatGPT に頼れば、ある程度の道を示してくれるので本当便利です。(機密情報を含んだコードを ChatGPT に流さないように注意してくださいね。)
recompose
を使ったコードはすべて削除できましたので、ライブラリも削除します。
11. react-redux などの状態管理を React Hooks で対応
小規模なサイトですが、一部 react-redux を使って状態管理をしていました。ただ、タブのアクティブ状態を管理しているくらいだったので、useState
と useContext
を使って状態を管理する Custom Hooks と Provider コンポーネントを作るやり方にしました。
ここでは具体的にどういうコードに置き換えたかは書きませんので、気になる方は以下の記事などを参考にされてください。
これで react-redux
などの状態管理周りのライブラリも削除できます。
ここまでくれば、ほぼ今まで通りに動作するようになります。ここからはそれ以外のツールのアップデート作業です。
12. ESLint を最新バージョンに。
ESLint は v4
が入っていたので、最新の v8
を使うようにします。
ルールは eslint-config-airbnb を使っていたのですが、最近更新が止まっているようなので、Next.js 向けの eslint-config-next を使うようにしました。
eslint-config-next
を使う設定を .eslintrc
で設定してあげるだけです。 Next.js は Core Web Vitals に関するルールが追加された next/core-web-vitals
の利用を推奨しているようなのですが、早くアップデートを完了したかったのもあり、今回は使用していません。
{
"extends": "next",
}
Next.js の ESLint を使うことで、@next/next/no-img-element のエラーが大量に出ることになります。これは next/image
の Image
コンポーネントを使ってね、ということなのですが、このプロジェクトはビルドしたファイルを export して利用する、いわゆる SSG なので、この Image
コンポーネントが使っている Image Optimization API は export されたファイルでは使えません。
なので、img
タグはそのままで、この @next/next/no-img-element
を無効にするルールを .eslintrc
で設定します。
{
"extends": "next",
"rules": {
"@next/next/no-img-element": "off"
}
}
13. jest を最新バージョンにアップデート
jest はひとつの単体テストくらいしか使っていなかったのですが、せっかくなのでアップデートします。v20
-> v29
へのアップデートです。
公式ドキュメント通り、next/jest
を使うやり方で、必要なライブラリをインストールし、jest.config.js
を作って設定します。
公式ドキュメントと同じやり方で設定しただけなので、詳しい解説は省略します。
14. その他必要なライブラリをアップデート
上記以外にも、このプロジェクト特有のライブラリがいくつかインストールされていたので、これまで通り「必要のないライブラリは削除。必要なライブラリは最新にアップデート」を繰り返し、すべてアップデートされた状態になりました。
これからは、ついでに TypeScript 化と、まだ手付かずの Storybook をどうにかします。
15. TypeScript 化
まずは、TypeScript をインストールし、.js
ファイルをすべて .ts
か .tsx
に変更します。
あとは公式ドキュメント にもあるように、next
などのコマンドを打てば勝手に tsconfig.json
などの必要な設定ファイルを作ってくれます。本当 Next.js は気が利いてて好きになるわぁ。
先ほど babel の項で書いたエイリアスの設定も忘れずに。
{
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
あとは、Flow を削除したときにコメントアウトした分を、TypeScript の書き方で書き換えるだけですね。他にもエラーはもちろん出ましたが、そこまでハマることなく解消できました(細かいエラーは忘れた😂)
16. Storybook が動作するように修正
最後に Storybook がきちんと動作するように設定ファイルなどを修正します。正直小規模サイトなので Storybook はそこまで必要ではないので、削除しようかとも考えたのですが、今まで通り動作するを目標に頑張ってきたので、ここまできたらやるかという気持ちでした。
ほぼ作り直しみたいなものなので、設定ファイルは一度削除し、npx storybook@latest init
で初期化しました。あとはこのプロジェクトに必要な設定を .storybook/main.ts
と .storybook/preview.ts
に入れてあげるだけです。
main.ts
には、静的ファイルディレクトリの設定と、import
のパスエイリアスの設定を入れます。以下のような感じです。
const path = require('path');
const config: StorybookConfig = {
stories: ["../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
staticDirs: ['../public'],
docs: {
autodocs: "tag",
},
webpackFinal: async config => {
config.resolve = config.resolve || {};
config.resolve.alias = {
...(config.resolve.alias || {}),
'@': path.resolve(__dirname, '../src'),
}
return config;
}
};
export default config;
preview.ts
には styled-component や GlobalStyle を適用させるための設定を decorators
オプションに入れます。
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
options: {
storySort: {
order: ["Atoms", "Molecules", "Organisms", "Templates", "Pages"],
},
},
},
decorators: [
(Story: any) => (
<ThemeProvider theme={theme}>
<GlobalStyles />
<Story />
</ThemeProvider>
),
],
};
export default preview;
storySort
の設定も Atomic Design を使っているのであればおすすめの設定です。以下の記事で解説しています。
これで、設定は完了なので、あとは、各 .stories.tsx
ファイルを、新しい Storybook の書き方に変更するだけです。
StoriesOf
を使う書き方からまったく書き方が変わっているので、具体的には説明しませんが、以下のように変更する感じですね。(もう書くの疲れた)
import React from 'react';
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import Hoge from '.';
storiesOf('Atoms.Code', module)
.add('default', () => (
<Hoge text={text('text', 'lorem ipsum.')} />
));
// を以下に変更
import React from "react";
import { StoryFn, Meta } from "@storybook/react";
import Hoge from ".";
type HogeProps = React.ComponentProps<typeof Code>;
export default {
title: "Atoms/Hoge",
component: Hoge,
} as Meta;
const Template: StoryFn<HogeProps> = (args) => <Hoge {...args} />;
export const Default = Template.bind({});
Default.args = {
text: "lorem ipsum.",
};
全コンポーネント書き換えないといけないので、地味で大変な作業ですが、これを乗り越えて Storybook の最新版で立ち上げることができました。
17. 完了!
npm audit
を実行すると...
うおおおおおおお!ついに vulnerabilities がついに 0
に!!🎉こうやって結果が見えると達成感ありますね。これからも 0
vulnerabilities をキープするぞー!
まとめ
とにかく作業もこの記事の執筆も疲れましたが、最終的にセキュリティリスクもなく、完全にモダンな開発環境が作れたことで達成感を味わえました。
最初にも書いた通り、大規模プロジェクトだとこうはいかないので、メジャーアップデートがあった時には面倒くさがらず早めにアップデートを心がけましょうね。そうすることでセキュリティアップデートにも柔軟に対応できるようになり、安心して開発をすることができます。
今回、アップデートの具体的な手順まで書きましたが、プロジェクトそれぞれでやることは違うと思います。とりあえず、この記事で言いたかったことはアップデートはいいぞです。
あと、今回の作業をやったことで Next.js とさらにお友達になれた気がします。また、こうやってアウトプットすることで、「なんかしらんけど上手くいった」的なものも、なぜ上手くいったのか深ぼってから記事を書く必要がありますので、こうやってアウトプットすることって本当大事ですね。
最初にアドバイスをいただいた ChatGPT にも報告しておきました。
長文読んでいただき、ありがとうございました☺️
Discussion