初めに
- Nextは新しい機能が次々出てきて魅力的だけど,Next12からNext13の壁は高い
- 我々のチームでは,プロダクトの価値を高めることというより,新しい技術の関心を高めることを目的として,新バージョンを使うことを決定した
- 5月から8月の期間を経てNextJSのプロジェクトの全構成をNext13に移行させた
- 大体500ファイルと70ページ, 80,000行のプロジェクトからなる
- 移行を経て得た経験や知見について要所5個まとめた
要所
1.移行することの価値をBiz側に説明できること
2.運用する環境の調査
3.移行戦略を立てること
4.ライブラリの依存関係
5.技術的新規ポイントは頭の中にインプット
1.移行することの価値をBiz側に説明できること
技術的挑戦とはいえ,App Dir移行によるプロダクトの価値は,開発以外にも共有できるようにすること
- 移行後の成果は,表面には現れない
- 開発以外のメンバーには,よくわからないことをしているだけであり,不透明な成果物の取り組みに対して,すれ違いが起こらないように取り組みの価値を共有することは重要
- 実際に開発とBizで共有認識として持っていた2例を紹介する
A. 再現の共有が早くなるよ
操作を共有するのでなく,URLを共有するだけで,その現象に辿り着くことができるようになり,顧客とのやり取りやチーム間で問題の共有が早くなることを伝えた.
例:特定の条件で発生する問題を共有したいとき
移行前
1 ユーザページ https://OOO.jp/usersに移行してください
2 次にユーザAの詳細ボタンを押してください
3 次に更新モードに切り替えてください
4 更新ボタンを確認してください
移行後
1 URL https://OOO.jp/users/1/updateの更新ボタンを確認してください
B. ユーザ依存の問題を減らせる
計算や表示する責務をユーザ端末でなくて,サーバ側に持たせることで,ユーザの端末依存の謎のバグを減らせる.
- 現にユーザ端末のバグは一回の調査で数時間とられ,再現不可な問題もあるため,原因不明の端末依存として結論づけることもあった
- 各処理に対してのバリデーションやローディングの処理でCSRの構成でも対応はもちろん可能である
- しかし,既存のページやこれから追加する新機能に対しての考慮対象から本問題のリスクを省きたい意図がありそのことを伝えた
2.運用する環境の調査をすること
直近技術なので特定のインフラに対して,対応策が必ずしもあるわけではない. 色々なインフラで発生している問題を把握して,各々で解決することが求められる.
例えば:
- Amplifyでは,一定のファイルサイズを超えると500エラーが出る
- Amplifyでは,特定のバージョンでDynamicRouteは機能しない可能性がある
- Netlifyでも同様にDynamicRouteが機能しない,Prebuildのスクリプトを追加すると機能する ← Amplifyで同様に発生する問題も解決できます!
などなど,利用するデプロイサービス以外のサービスについても幅広く知識を集めることをお勧めします.
3.移行戦略を立てること
我々が定めていたおおまかなルールは以下ステップで移行を行うことです.
- Next13にバージョンアップと空のApp Dirを作る
- Head情報など
_document.tsx
の情報をAppのlayout.tsx
で再現して配置する - HeaderやFooter,SidebarやProviderなど元々のPageでLayout的な役割をしているものをAppの
layout.tsx
で再現 - 新規ページはApp Dirで開発する
- 既存機能は,追加開発時にApp Dirへ移行
- 既存機能で,触ることの少ない機能は,時間があるときにApp Dirへ移行
- 新規開発が少なくなるタイミングで,残りの既存機能を徐々にリプレイス
※残りの移行をスムーズに行うために3の時点で,なるべく多く各階層に仮想的なLayout(パス名を変えたりして同じレイアウトになるようなもの)を配置しておくことをお勧めします!!
4.ライブラリの依存関係を考慮すること
react18が必要になる
対応するreact-domやeslint,tsについても適切なバージョンになっているか確認は必要,またそれらに対応したnodeのバージョン変更も必要になる
next-authのapiは動作する.でも,export部分を考慮する必要がある
// app/api/auth/[..nextauth]/route.ts
const authOptions: AuthOptions = {...}
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
i18n系ツール
- next-intlは対応している
- next-internationalは対応している
- i18nextは対応している
- rossetaは3年くらい更新ないけど大丈夫か,Next依存してないので問題ないのか
- など
また,使っているツールのドキュメントでmiddleware.ts
についての説明を確認し,自身のサービス通り動くかどうかは,あらかじめ調査しておくこと
UIライブラリ
- Theme Providerは
layout.tsx
に配置して問題ないか? - 微妙な色の変化やUIのずれは生じていないか?
5.技術的新規ポイントは頭の中にインプット
よく使う新技術に対するコードスニペットやよく発生するエラーのパターンは,容易に解決できるようにするため最低限の知識は把握しておくことをお勧めします.私が把握しているものの例は以下のものです.
Pageでのパラメータの取得
param(パスの中のID)とsearchParam(クエリの値)を取得できる
// /[id]/aaa?year=2024
const Page = async ({
params: { id },
searchParams: { year = '2022' },
}: {
params: { id: string };
searchParams: { date: string };
}) => {
return <>Hello</>;
};
export default Page;
データの取得
awaitの待ち時間を極力無くすように1行ずつでなく1箇所で呼び出すようすること
const Page = async ({
params: { id },
searchParams: { year = '2022' },
}: {
params: { id: string };
searchParams: { date: string };
}) => {
const _res1 = fetch(`/api/test`);
const _res2 = fetch(`/api/test2`);
const [res1, res2] = await Promise.all([_res1, _res2]);
return (
<>
{res1}
{res2}
</>
);
};
export default Page;
LayoutはParamsのみ取得できる
サーバーレイアウトで,searchParamを取得したいようなLayoutの場合は,Parallel Routes
を利用すれば実現できる.
const Layout = async ({
children,
params: { id },
}: {
children: ReactNode;
params: {
id: string;
};
}) => {
<>{children}</>;
};
export default Layout;
Parallel Routes
よく使うParallel Routesの用途は2つ
-
searchParams
を使いたいレイアウトの要素があるとき - モーダルのようにメインPageから独立した部分をパスレベルで切り離したいとき
※これ以外の用途で使いたい場合は,RSCで解決できることが多い
ツリー構造は把握しておこう
ツリー構造
├─ /test
│ ├─ page.tsx
│ ├─ layout.tsx <- モーダルの配置
├─ ├─ default.tsx <- 同階層のpage.tsxを返す
├─ ├─ @modal
| │ ├─ default.ts <- nullを返す
│ │ └─ [id]
└─ page.tsx <- モーダルの中身 モーダルはopen=trueで固定で良い
レイアウト
// layout.tsx
const Layout = async ({
children,
modal,
params: { id },
}: {
children: ReactNode;
modal: ReactNode;
params: {
id: string;
};
}) => {
<>
{children}
{modal}
</>;
};
export default Layout;
@modal
の中身となるページ
// @modal/page.tsx モーダルの表示非表示は,layoutがするので常にtrueで良い
const Page = () => <Modal open={true}>内容</Modal>;
export default Page;
default部分を定義してないとエラーが出る
エラー発生して,修正した場合は,開発モードnpm run dev
or yarn dev
でのホットリロードが効かない場合あるので,再度アプリを立ち上げることを推奨します.
// default.tsx 上記階層のpageを返すファイル
import Page from './page';
const Default = (props: any) => {
return <Page {...props} />;
};
export default Default;
// default.ts 上記階層のnullを返すファイル
const Default = () => {
return null;
};
export default Default;
同階層で一部だけ特定のレイアウトを使いたい
# ページのパスは崩さずCとDだけ違うレイアウトを使いたい
├─ A
├─ B
├─ C
├─ D
├─ layout.tsx
# (CD)丸括弧を使うことができる
├─ A
├─ B
├─ layout.tsx
├─ (CD)
├─C
├─D
├─ layout.tsx
├─ /layout.tsx
use client
を使うルールをあらかじめ統一する
ルール例
- onChangeやonClickがある部分のコンポーネントのみ'use client'の別ファイルで切り離す
- ButtonやFormのみ切り離す
- Provider系は,layout的な感じで
use client
で切り離す'use client' const Provider = ({ children }) => { return <AAAProvider>{children}</AAAProvider>; };
などなど,他にもIntercepting Routesやserver action, Turbo packなど使われることが少ない機能,ツールに対しても,最低限のルールや使い方を知っておくことをお勧めします.
最後に
新しいバージョンへの移行は一時的な作業ではなく,長期的な視点で考えるべきです.
新しいバージョンへの移行に際して、新機能の導入が必要でなく、単に既存のpageから新しい app ディレクトリに移動するだけでも、新機能への対応が可能です。アプローチ次第では,リスクを最小限に抑えつつ新しいバージョンに移行できます.
Next.jsは新しい機能やパフォーマンスの向上を提供する一方で,新しいバグも多く発生しています.したがって,プロジェクトの現状の安定性を維持しつつ,新しいバージョンへの移行計画をしっかりと練ることが重要です.長期的な視点を持ちつつ,自身のチームのプロジェクトの進行に柔軟に対応できる戦略を検討することが成功の鍵となるでしょう!