【Next.js】App RouterはAWS Amplify Hostingに何をもたらすのか?
はじめに
こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!
この記事では、Next.js v13.4からStableになったApp Routerについて解説します。
Amplify Hosting + App Routerに興味がある方の参考になれば幸いです。
目次
- Next.jsとAWS Amplify Hosting
- Pages RouterとApp Routerの比較
- 実験環境の構築
Next.jsとAWS Amplify Hosting
Amplify DocumentationというAWS Amplifyに特化したドキュメントサイトがあります。
この中にはNext.jsのGetting startedがあるのですが、一部記述がこのように更新されています。
Amplify JavaScript works best with the Next.js Pages Router. Full support for the App Router introduced in Next.js 13.4 is coming soon.
またSSRに関する情報をまとめている場所では、このような項目が追加されています。
Use Amplify with Next.js App Router ("app/" directory)
さらに、公式ドキュメントのNext.js feature support 欄にもNext.js 13 app directory (beta)
が追加されています。
つまり、
AWS Amplify HostingでApp Routerが使えるようになってる!?
Pages RouterとApp Routerの比較
実際に確かめていきます!
各特徴を表現しやすくするために、データフェッチに3秒かかるように設定しています。
またApp RouterではSuspenseのfallbackを設定し、データフェッチ中fetching...
と表示されるようにしています。
(構築方法や詳細な設定は、次章をご覧ください。)
リモートサーバ
AWS Amplify Hostingで払い出されるURL(https://<ブランチ名>.<ID>.amplifyapp.com)にアクセスして、各特徴を確認してみます。
Pages - Static Site Generation(SSG) | Pages - Server Side Rendering(SSR) | App - Server Component (RSC) | App - Client Components (RCC) |
---|---|---|---|
SSGとSSRは想定通りの挙動です。
またRCCも想定通り、データフェッチ中fetching...
と表示してくれています。
一方でServer Componentに詳しい人は「あれ?」と思ったと思います。
RSCは、RCCと同様にfetching...
が表示されることを期待していたからです。
さて、どこに問題があるのでしょう?
ローカルサーバ
AWS Amplify Hostingのビルド結果は、Localで動かすことが可能です。
AWS Amplifyのマネージメントコンソールの構築
タブのダウンロード
から入手可能です。
入手したZipファイルから、以下の手順でサーバーを起動します。
$ unzip artifacts.zip -d artifacts
$ cd artifacts
$ node server.js | open http://localhost:3000/
ローカルサーバで起動した結果がこちらです。
Pages - Static Site Generation(SSG) | Pages - Server Side Rendering(SSR) | App - Server Component (RSC) | App - Client Components (RCC) |
---|---|---|---|
こちらは期待通りRSCもfetching...
が表示されているようです。どういうことでしょうか?
AWS Amplify Hostingのサーバ内部が公開されてないので完全な妄想になりますが、サーバからのStreaming処理を許可せず全ての処理が完了してから配信される設定になっているのかもしれません。
このことは我々を少し混乱させます。
そもそもApp Routerを使ってやりたいことは、「Pages RouterのSSRでデータフェッチが重たい場合、ページ全体の表示が遅くなる問題を解消すること」です。その一方で現状のAWS Amplify HostingでのRSCの挙動はSSRと変わりません。(なんなら表示速度は悪化しています。)
では、RCCを積極的に使えば良いのか?それもまたケースバイケースとなります。どのようなデータをいつフェッチしなければならないかに依存するからです。もし秘匿性の高いデータであれば、クライアントサイドからのデータフェッチは許可されるべきではないです。
実験環境の構築
実験環境を構築していきます。
Next.js + AWS Amplify Hostingの詳しい解説は以下の公式ブログが参考になります。
まずはcreate-next-appでプロジェクトをinitしようと思います。
init
まずはPages Routerでinitします。
実は、App RouterとPages Routerは共存することが可能です。
$ npx create-next-app
✔ What is your project named? … my-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? …Yes
✔ Use App Router (recommended)? … No
✔ Would you like to customize the default import alias? … Yes
出来上がったファイル群から不要なものを削除していきます。
import Link from 'next/link';
const Home = () => {
return (
<main>
<p>Hello !</p>
<Link href='/ssg'>to SSG</Link>
<br />
<Link href='/ssr'>to SSR</Link>
<br />
<Link href='/rsc'>to RSC</Link>
<br />
<Link href='/rcc'>to RCC</Link>
</main>
);
};
export default Home;
- import '@/styles/globals.css'
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
API
APIは、リクエストから3秒後に結果が返ってくるように設定します。
import dayjs from 'dayjs';
import { NextApiRequest, NextApiResponse } from 'next';
type TestRes = {
now: string;
};
const handler = async (req: NextApiRequest, res: NextApiResponse<TestRes>) => {
// 3秒間の処理停止
await new Promise((resolve) => setTimeout(resolve, 3000));
// API実行時刻を取得
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
res.status(200).send({ now });
};
export default handler;
Pages Router
次にsrc/pages以下にAPIとページを追加していきます。
SSGとSSRの違いは、データ取得にgetStaticPropsとgetServerSidePropsのどちらを使用するかで決まります。
Page
SSGになるようにgetStaticPropsを設定します。
import dayjs from 'dayjs';
import Link from 'next/link';
const SSG = ({ now }: { now: string }) => {
return (
<main>
<p>Hello SSG!</p>
<p>Now - {now}</p>
<Link href='/'>to Home</Link>
</main>
);
};
export const getStaticProps = async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
return {
props: {
now,
},
};
};
export default SSG;
SSRになるようにgetServerSidePropsを設定します。
import Link from 'next/link';
const SSR = ({ now }: { now: string }) => {
return (
<main>
<p>Hello SSR!</p>
<p>Now - {now}</p>
<Link href='/'>to Home</Link>
</main>
);
};
export const getServerSideProps = async () => {
const data = await (await fetch(`http://localhost:3000/api/test`)).json();
const now = data['now'];
return {
props: {
now,
},
};
};
export default SSR;
App Router
次にsrc/app以下にページを追加します。
App Routerには、React Server Component (RSC) という機能が搭載されています。
RSCの詳細な説明は公式サイトの方に譲ります。
実装する上で気をつけなければならないのは、以下3点です。
- Server ComponentsとClient Componentsが存在する
-
'use client'
ディレクティブを明示的に宣言しない限り、Server Componentsとして扱われる -
useEffect
やonclick
などクライアントサイドでの実行が求められる場合はClient Componentsでなければならない
App Routerは、app/<path>/
以下にlayout.tsx
とpage.tsx
を配置することが基本になります。
Server Component
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='en'>
<body>{children}</body>
</html>
);
}
async Componentは現状型チェックで失敗してしまうようで、@ts-expect-error
で強引に突破する必要があるようです。
import Link from 'next/link';
import { Suspense } from 'react';
import ServerComponent from './ServerComponent';
const RSC = () => {
return (
<main>
<p>Hello RSC!</p>
<Suspense fallback={<p>fetching...</p>}>
{/* @ts-expect-error Async Server Component */}
<ServerComponent />
</Suspense>
<Link href='/'>to Home</Link>
</main>
);
};
export default RSC;
Server Component内部でfetchします。
今回はわかりやすさのため、cacheしない設定にします。
const ServerComponent = async () => {
const data = await (
await fetch(`http://localhost:3000/api/test`, { cache: 'no-store' })
).json();
const now = data['now'];
return <p>{`Now - ${now}`}</p>;
};
export default ServerComponent;
Client Component
Client ComponentはServer Componentとほとんど同じですが、ClientComponent.tsxだけクライアントサイドで実行されるように'use client'
ディレクディブを設定します。
'use client';
const ClientComponent = async () => {
const data = await (await fetch(`/api/test`, { cache: 'no-store' })).json();
const now = data['now'];
return <p>{`Now - ${now}`}</p>;
};
export default ServerComponent;
AWS Amplify Hosting
AWS AmplifyのマネージメントコンソールからGitリポジトリ連携し、Next.jsセッティングでデプロイするだけです。
詳細な手順は、先ほどのAWS公式ブログをご参照ください。
最後に
ここまで読んでいただきありがとうございました。
これまでのSSGとSSRの使い分けは非常に明快でした。それが「要件によって選べるようになる = 何を選択すべきか複雑性が増す」ことになるでしょう。
そのデータは静的であるべきか?サーバサイドでフェッチすべきか、クライアントサイドでフェッチすべきか?表示されるまでの速度は?
すぐに全面的にApp Routerに移行すべき、のような単純な話ではなさそうです。
ただしcoming soon.
と記載されていることから、今後RSCの挙動が改善される可能性はあります。期待しましょう。
この考察が皆様の一助となれば幸いです。
Discussion