nzws.me
Next.js 13 に移行したメモ
#雑記
#TypeScript
#Next.js
#Edge
#Vercel
#Satori
Sunday, March 12, 2023

どうもこんにちは、@nzws です。このウェブサイト、nzws.me を Next.js v13 に対応するついでに新機能を使う目的で全体的に書き直しました。

Next.js の経験は?

趣味とか個人的なプロジェクトだと結構数年前より採用しています。 今公開しているものとしては、nzws.me(ここ) とか、KnzkLive の Web インターフェイスとかです。 実務では使ったことないです。

Next.js 13 の変更点とか

https://nextjs.org/blog/next-13

App Router の誕生

一番でかいのは App Router のサポートですね。 今まではファイルベースのルーティングで pages/ 下に任意の JavaScript ファイルを置くことで、それがそのままパスになってました。

例: pages/index.tsxexample.com/ , pages/foo/bar.tsxexample.com/foo/bar

この pages/ ディレクトリの個人的に意識している問題点としては、ルーティングとは関係ないファイルが近くに配置できない(例えば、テストファイルやページのコンポーネント) ので別で components/ tests/ ディレクトリを用意したり、Next.js の設定を変更してページファイルの命名規則を変えたりして対応する必要がありました。 また、ページに紐づく形でファイルが直接マウントされてしまうため、ページをまたいでマウントしたい Provider のようなコンポーネントは pages/_app.tsx で共通でマウントする事が可能でしたが、 プロジェクトが進むにつれて大きくなったり柔軟な対応ができないみたいな問題点もありました。

そこでこれらの問題を解決する新しいルーティングの戦略が App Router です。 App Router は pages/ に変わり app/ 下に配置します。

pages/ とは異なり、App Router では直接ページとしてマウントするファイルを page.tsx として配置します。

例: app/foo/bar/page.tsxexample.com/foo/bar

また、ディレクトリごとに layout.tsx というファイルを配置でき、それより下の階層に対して _app.tsx 的なノリで共通でマウントできるコンポーネントを実装できます。

その他いくつかのファイルを除き、 ファイルベースのルーティングとしては全体を見ないようになるので、コンポーネントとかテストファイルとかを同じ/近くのディレクトリに気軽に配置することができるようになるのが、App Router の嬉しいポイントです。

また、App Router を使用することによるもう一つ大きな特徴として、React Server Components をデフォルトで使用するようになります。

ここらへんは理解が浅いんですが、Server Components は React を UI のテンプレートエンジン的な感じで完全にサーバー上で組み立ててしまい、 クライアントには JavaScript 無し、HTML のみで渡す事でパフォーマンスを向上させる事ができるみたいな感じだと思います。

pages/ ではそんなもの意識した事が無かったのでデフォルトでクライアントコンポーネントだと思いますが、 App Router ではデフォルトでサーバーコンポーネントを用いるようになります。(コンポーネントごとに選択することができます)

ただ今回 nzws.me を開発するに当たり一通り触ってみたものの、 動的なものが一切使えない事や新しい概念という事もあり、それぞれのコンポーネントの考慮がしづらく、現状自分の中では微妙な評価です。

サーバーサイドで完結する html を生成するのでそれはそうって感じですが動的に動く Hook が一切使えなかったり、 後述の CSS-in-JS が使えないとか、サードパーティのライブラリを使おうとするとそのライブラリもサーバーコンポーネントに対応している必要がある (一応無理やりラップしてクライアントコンポーネントとして使うことはできるらしい)とか、みたいな点があります。

Turbopack アルファ版の導入

Rust ベースのバンドラーである Turbopack が追加されました。まだアルファ版で開発サーバーぐらいしか動かないそうです。 姉妹プロダクトの Turborepo は結構色々なところで使っているのですが、Turbopack はまだアルファ版という事もあり全然使えておらず、 安定版が楽しみです。(一時期 10x faster than Vite とか Vite を煽りすぎて炎上してたけど結局あれどうなったんだろう…)

あとは細かいところで Web フォントの簡単&高速導入サポート機能とか追加されています。一応使っています。

Next.js 12~13 までに使いこなせていなかった Edge Runtime とか

個人的に Edge Runtime 周りの機能が 12~13 あたりで色々増えた認識ですが、Middleware ぐらいしか使ったことがなかったです。

Next.js の Middleware は KnzkLive でローカライズごとのパス変更みたいなので若干かじっていました。 その後 Edge Runtime 上で Middleware に限らず普通のサーバーレス関数として動かす Edge API Routes ができたそうですが、こちらは触ったことが無かったです。

今回はインタラクティブな検索 API で Edge Runtime を活用していますが高速で問題なく動いてくれているので良い感じです。 (ファイルアクセスなどは Edge でできないので普通に Node.js のサーバーレス関数で API を動かしています)

Vercel にて Edge Function のリアルタイムログの様子

Vercel にて Edge Function のリアルタイムログの様子

rm -rf nzws.me/pages && mkdir nzws.me/app

そもそも App Router 自体はベータ版なのでオプトインですし、有効にしたとしても段階的に移行できるように pages/ と共存して動かす事が可能だそうです。 ただ、(nzws.me が自分の試験場という意味合いもありつつ)Next.js 13 の機能を存分に体験したかったり、このサイトも数年経過してきて UI を色々見直したかったので全部書き直しました。

最初に躓いた点はサーバーコンポーネントでは CSS-in-JS を使用できない所です。(もちろんクライアントコンポーネントでは引き続き使用可能です) まあ、CSS-in-JS 自体すごいインタラクティブに動くのでどうしようも無い感はありますが、一応使いこなしたかったので scss で全て書き直しました。

scss 自体久しぶりに触る(というか React ではずっと styled-components しか触ってなかった)んですが、CSS Modules という概念?を初めて触ってみました。 JavaScript 上で module.scss ファイルをインポートするとクラス名のオブジェクトが入ってそのまま JSX 上で className に代入できるみたいな感じでしたが、 型定義が弱く、やはり CSS-in-JS に沼っていた人間としては細かいことやろうとすると不便だなあみたいな感想を持ちました。

結局色々やろうとするとクライアントコンポーネントにする必要があり(そのためサーバーコンポーネントの利点を感じられなかった)、 ドキュメントに記載されている分には CSS-in-JS 周りのサポートもいい感じにしていくそうなので、正直どうでも良かったかもしれません。

散乱するディレクトリの様子(死)

散乱するディレクトリの様子(死)

サーバー上での情報取得 (Fetching)

サーバーサイドで情報を渡すためにコンポーネント上で単純に非同期関数を叩くことができるようになりました。 今までの Next.js では恒例だった(それでいて多分初心者は使い分けに頭を悩ませる) getSeverSideProps getStaticProps (と昔使われていた getInitialProps )あたりの API は全て App Router では無くなり、 単にコンポーネントを非同期関数にして、例えば fetch API であれば const data = await fetch(...) をレンダリングする関数内で叩くだけで良くなりました。

また、今までは静的な(ブログのような)データは getStaticProps で取得し、リアルタイムに変化してほしい、もしくは認証が必要なデータは getSeverSideProps で取得のような使い分けをしていましたが、 fetch では fetch API 上にオプションとして再検証タイミングやキャッシュ可否を入力するようになり、 また Cookie や Header といったユーザーによって変わり得る情報をサーバー側で要求した場合に自動的に動的なデータとしてマークするそうです。(試してないですが)

あとこれまた理解の浅い部分ですが Streaming という機能により ページのルートに限らず個別のコンポーネントが非同期関数となることが可能になりました。(ただし現状、型定義がバグります) 上記のページと同様に個別の Next.js にマウントしたコンポーネント上で非同期関数の情報を取得し、そのまま使用することができるようになります。 また、非同期関数であるため当然待機時間が存在しますが、その間のローディングやスケルトンを指定する方法として Suspense コンポーネントが使用できます。 この機能により純粋な API からの情報表示がだいぶいい感じに React 上で実装する事ができるようになった印象です。

(App Router の)バグ踏んじゃった

どうでもいいポイントですが新機能を組み合わせて使っていたらバグりました。 具体的にはまず App Router に Route Groups という機能を使用していました。 違うパスの階層でもかっこでくくったディレクトリの下に配置することで見えないパスのようなものが作られ、簡易的にまとめて Layout などを定義する事ができます。

例: app/(hoge)/foo/page.tsx & app/(hoge)/bar/page.tsx/foo & /bar

また、もう一点 Edge 上でページを動かす設定に変更していました。ページはページごとにサーバーサイドをどこで実行させるか指定することができます。 デフォルトは Node.js ですが、 experimental-edge で Edge Runtime 上で実行することもできます。 前述の通り基本的にページは HTTP API しか叩いていないので、全世界で低遅延で実行できる Edge を全ページで選択しました。

ところが、恐らくこの 2 つの組み合わせにより Vercel 上にデプロイした時だけ self is not defined というエラーを出すようになりました。 この条件が記載されている Issue が執筆時点で Open のまま残っており、恐らくこの条件で問題がありました。

今の所改善しないので、必須ではなかった Route Groups を使用しないようにしました。 まあ、Experimental ばかり付いてる機能を使いまくってるので、しょうがないかなぐらいの思考でやっています。

(Next.js 関係なく) cmdk を使ってみた

https://github.com/pacocoursey/cmdk

元 Vercel で現在は Linear で働いているらしい @pacocoursey 氏の React ライブラリです。 いい感じにウェブ上でコマンドメニューを作成することができ、実際に vercel.comlinear.app 上でも使用されています(Ctrl/Cmd+K で動くはずなのでやってみてください)

ただ誤解していた点としてはライブラリ自体はスタイルは一切提供されていないので、リポジトリにあるサンプルを引っ張ってきていい感じに加工して対処しました。

nzws.me では簡易的なナビゲーションや記事検索フォームとして活用しています。 若干躓いた点としては、cmdk 上の input で Enter を叩くと IME のバッファ中であっても選択中のアイテムの選択コマンドが発火してしまうという CJK 環境が考慮されてないあるあるな問題が発生しましたが、 keyDown 時に e.isComposing を見てバッファ中であればイベントを防止するようにしました。

記事検索自体は裏側の Node.js API でファイルアクセスし全文を取得し、Node.js API と通信した Edge API 上で全文をキャッシュしつつ絞り込んで最終的にクライアントに返しているので、 爆速だしクライアントも軽くていい感じに動かせています。

cmdk の使用例

cmdk の使用例

マークダウンデータの読み込み

このブログは基本マークダウンで管理し Node.js 上で解析してデータ渡して React 上に読み込ませていますが、 そのプロセス自体は以前と変化無いです。いつも通り unified/remark/rehype のエコシステムに乗っかっています。unified 最強。(ただもしかしたら気まぐれでそのうち MDX に変えるかもしれない…)
MDX の誘惑に負けて MDX に載せ替えました(hashicorp/next-mdx-remote)グッバイ unified...

next-mdx-remote はバックエンドで MDX (Markdown+JSX) の文字列をシリアライズさせてフロントエンドのコンポーネントに突っ込むと特に面倒な事をしなくてもマークダウンが描画でき、 かつ MDX の利点でカスタムの React コンポーネントを追加したり標準のタグを簡単にカスタマイズしたりできます。
特に Next.js だとマークダウンに書いた画像タグをそのまま next/image に差し込むようにカスタムできるので便利です。

実は以前別のプロジェクトでこれ自体は使ったことあるのですが、今回 App Router で試そうと思ったところ不安定版としてサポートされており、なおかつ App Router 自体がシンプルになったので mdx-remote のコンポーネントの操作もシンプルになっていました。(Edge Runtime では動きませんでした。残念)

Satori で SEO 系の画像を出力する

https://github.com/vercel/satori

Vercel が開発する画像生成ライブラリです。Next.js で導入する際のパッケージ名が @vercel/og なので、 主に OpenGraph 用の画像を動的に生成するツールかと思っています。 html5 canvas とかでゴリゴリ職人芸をせずとも React JSX でいい感じに書くことができ、 必要機能も一通り揃っている印象なのでいい感じです。 また、Vercel が開発しているだけあって Edge 系でも動くように作られているので安心して使用できます。 OGP で使う画像を加工したり、画像アセットが無いときに雑にテキストの画像を返す目的で使用しています。

おわりに

こういう事やってると、もはやよくわからない謎に PHP のランタイムが爆速な国内のレンタルサーバーに PHP 置いとけば(つまり WordPress)楽なのでは…? みたいな気持ちに若干なったりしますが、趣味のウェブサイトの最適化は完全にロマンな分野ですよね。 まあ、商用システムになってくるといい感じにスケーリングとか CDN 配信とかセキュリティ…とか色々考えると現代のベストプラクティスかとは思いますが。。
ちなみに今回使った next-mdx-remote の README にこう書かれていました。

これでどうやってブログを作ればいいのか?
開発者向けツールの使用例の 99%は、不必要に複雑な個人ブログの構築であることがデータで示されています。冗談です。しかし、真面目な話、個人または中小企業でブログを構築しようとしているのであれば、通常の html と css を使用することを検討してください。シンプルなブログを作るのに、重いフルスタックの javascript フレームワークを使う必要はありません。数年後にアップデートをするために戻ってきたとき、依存するものすべてに 10 回の破壊的なリリースがなかったら、後で自分に感謝することになるでしょう。

(DeepL で翻訳: How Can I Build A Blog With This?)

あと春休み中は (Pure な方の) React エンジニャとしてお仕事をしつつ隙間の休日とかでこれ作ってましたが、 3 月ももうそろそろ半分過ぎ、学校もそろそろ始まりそうです。特に今年は三年次なので就活とか色々考えると胃が痛いです。疲れた(まだ始まってないけど)