📣

【Next.js】App RouterはAWS Amplify Hostingに何をもたらすのか?

2023/05/29に公開

はじめに

こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!

https://www.oto-trip.com/

この記事では、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の詳しい解説は以下の公式ブログが参考になります。

https://aws.amazon.com/jp/blogs/news/amplify-next-js-13/

まずは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

出来上がったファイル群から不要なものを削除していきます。

src/pages/index.tsx
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;
src/pages/_app.tsx
- import '@/styles/globals.css'
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

API

APIは、リクエストから3秒後に結果が返ってくるように設定します。

src/pages/api/test.ts
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を設定します。

src/pages/ssg.tsx
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を設定します。

src/pages/ssr.tsx
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の詳細な説明は公式サイトの方に譲ります。

https://nextjs.org/docs/getting-started/react-essentials

実装する上で気をつけなければならないのは、以下3点です。

  1. Server ComponentsとClient Componentsが存在する
  2. 'use client'ディレクティブを明示的に宣言しない限り、Server Componentsとして扱われる
  3. useEffectonclickなどクライアントサイドでの実行が求められる場合はClient Componentsでなければならない

https://ui.docs.amplify.aws/react/getting-started/usage/nextjs#app-router

App Routerは、app/<path>/以下にlayout.tsxpage.tsxを配置することが基本になります。

Server Component

src/app/rsc/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  );
}

async Componentは現状型チェックで失敗してしまうようで、@ts-expect-errorで強引に突破する必要があるようです。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#parallel-data-fetching:~:text=Async Server Component TypeScript Error

src/app/rsc/page.tsx
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しない設定にします。

src/app/rsc/ServerComponent.tsx
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'ディレクディブを設定します。

src/app/rcc/ClientComponent.tsx
'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