🥸

Learn Next.jsをやってみた

2024/01/14に公開

はじめに

この記事は、Lancers(ランサーズ) Advent Calendar 2023 の22日目の記事です。
本来公開すべき日付から3週間以上たっていますが、気にしないでください。(^^;)

フロントエンドエンジニアの谷(@high_g_engineer)です。
本記事では、Next.js 公式サイトにある Learn Next.js の各章の要約と感想を記載していきます。
日本語訳を活用できる箇所はそのまま引用させていただいています。

筆者のステータス

  • 本業で React + CakePHP、副業で Next.js を利用した開発を行っています
  • 元々は、Vueユーザーで、Vue3 のリリース付近で、React に乗り換えました
  • Next.js は10から案件利用しています(AppRouterはエアプ状態)

やっていき

導入

  • サンプルプロジェクトをもとに、Next.js 14 のページ作成、スタイルの書き方、ルーティング、CRUD、認証など、諸々を学べます
  • 全編英語で記載されていますが、翻訳機能で十分に理解できます
  • Node.js 18.17.0 以降が動作する環境が必要です
  • 最低限の HTML, CSS, JS, React の開発知識は合ったほうが良いです

https://nextjs.org/learn/dashboard-app

第1章 はじめる

  • この章では、学習をはじめるためのプロジェクトの準備を行います
  • 環境を準備のため、ターミナルに以下を入力します
npx create-next-app@latest nextjs-dashboard --use-npm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"
  • npm i でプロジェクトのパッケージをインストールします
  • npm run dev でプロジェクトの実行ができます(Node.18.17.0未満だと正しく起動しません)
  • 環境をインストールすると、ディレクトリ構造は以下のようになっています(一部省略)
├── app/
│   ├── lib/ - 再利用可能なデータ取得、ユーティリティ系の処理を配置します
│   ├── ui/  - UIコンポーネント全般を配置します
│   ├── page.tsx
  • これまでは、ルーティング関係のコンポーネントは pages/ 、それ以外のコンポーネントは適宜オリジナルのディレクトリを作成するルールでした
  • AppRouterの場合、コンポーネントをすべて app/ で管理するルールになっています
  • ディレクトリ構成が app/lib, app/ui となっており、シンプルで分かりやすいです

https://nextjs.org/learn/dashboard-app/getting-started

第2章 CSSスタイル

  • この章では、CSS Modules, Tailwind CSS, clsx を利用したスタイルの記述方法が学べます
  • global.css の様なファイルも /app/ui/ 以下に配置してしまってokみたいです
  • 公式が案内している通り、globals.css に以下を記述し、/app/layout.tsxでインポートすれば、Tailwindが利用可能です
@tailwind base;
@tailwind components;
@tailwind utilities;
  • 公式のとおりに記載すれば、Tailwind CSS の知識が無くとも、十分に便利さを体感できます
  • CSS Modules を利用したい場合も /app/ui/ 以下に ◯◯.modules.cssを配置すればokです
  • classNameの記述を条件式で切り替えたい場合は、 clsx というライブラリを利用すれば良いみたいです
  • 筆者は、classnames を利用していましたが、clsxの方が軽量かつ、公式が紹介していて良さそうなので、こちらに乗り換えようと思います

https://nextjs.org/learn/dashboard-app/css-styling

第3章 フォントと画像の最適化

  • この章では、Next.jsが標準で備える next/font, next/image の利用方法が学べます
  • フォントは、next/font を利用することで、ビルド時に静的アセット化することができます
  • おかげで、パフォーマンスに影響を与えるようなフォントの追加ネットワーク要求が発生しません
  • 画像は、imgタグの拡張である next/image が利用できます
  • next/image を利用することで、デバイス幅に応じた最適な画像が設定可能です
  • これまで、next/image にはあまり良い印象がなかったのですが、 Tailwind CSS との相性が良さげで、案件導入する際には積極的に使ってみたいと思いました

https://nextjs.org/learn/dashboard-app/optimizing-fonts-images

第4章 レイアウトとページの作成

  • この章では、Next.jsを利用したルーティング、ページ、レイアウトの作成方法が学べます
  • AppRouterのルーティングは、app/ 以下のディレクトリ構成に依存する仕組みになっています
  • 例えば、 app/dashboard/invoices/ といったディレクトリ構成の場合、/dashboard/invoices/ の部分がURLパスになります
  • 上記に加え、page.tsx というファイルが存在する位置が、アクセス可能なルートになります
  • 例えば、app/page.tsx/app/dashboard/page.tsx/dashboard がアクセス可能なルートになります
  • page.tsx には、通常のReactコンポーネントと同じような記述が可能です
  • 使い回したい共通レイアウトは、layout.tsx というファイルを作成することで対応できます
  • app/layout.tsx というファイルを作成した場合、プロジェクト全体で共通のレイアウトが作成できます
  • app/dashboard/layout.tsx というファイルを作成した場合は、 /dashboard/ 以下に存在するページのみで共通のレイアウトが作成できます
  • これまでの Next.js の場合、 pages/components/ を意図的に分けないといけない為、コンポーネントの管理を少し考えないといけませんでした
  • しかし、 app/ 内に存在する page.tsx が起点になってくれることで、管理がしやすくなったと感じています

https://nextjs.org/learn/dashboard-app/creating-layouts-and-pages

第5章 ページ間の移動

  • この章では、next/linknext/navigation を利用したページ遷移周辺の実装が学べます
  • ページ遷移は next/link を importし、Link コンポーネントとして扱います
  • Link コンポーネントは、aタグとほぼ同じ感覚で扱うことができます
  • Link コンポーネントを用いた場合、その箇所がビューポートに表示されたタイミングで、遷移先のコードをバックグラウンドで自動的にプリフェッチします
  • これにより、ページ遷移が爆速になります
  • next/navigationusePathname メソッドで、ページパスが取得できます(use client ディレクティブ必要)
  • next/navigation はこれまでのNext.jsでも感覚的に利用していましたが、サンプルコードのおかげで、秩序あるパス周辺のコードが書けるなと感じました

https://nextjs.org/learn/dashboard-app/navigating-between-pages

第6章 データベースのセットアップ

  • この章では、Vercelを利用したDBのセットアップ方法が学べます
  • まず、GitHubリポジトリを作成し、Vercelと紐づけます
  • 紐づけが完了したら、Vercel上でPostgresの設定を行います
  • 続けて、開発環境で .env を作成し、Vercelに表示されている .env.local の内容をコピーします
  • /scripts 以下に存在するseedファイルを利用し、初期データを作成します
  • 筆者は、VercelのDB周りはあまり活用したことがないのですが、サンプルに従うだけで、難しいという気持ちが打破できたように感じました

https://nextjs.org/learn/dashboard-app/setting-up-your-database

第7章 データの取得

  • この章では、6章でデータ取得に対するいくつかのアプローチが学べます
  • データ取得方法は2パターン存在します
    • APIレイヤーでRoute Handlers を利用する方法
    • APIレイヤーは利用せず、React Server Components(以下、RSC)で直接クエリを実行する方法
  • RSCを利用する場合、useEffect, useStateなどは利用せず、async/awaitで直接的にデータを取得るコードを書くことができます
  • これまでのNext.jsだと、useEffectを利用して、fetchのコードを準備するか、getServerSidePropsを利用するかが主流でしたが、RSCでは、直感的にfetchのコードを記述できるので、ものすごく開発体験が良いです
  • この章のサンプルコードでは、@vercel/postgressql をimportし、直接SQL文を記述するように紹介されていますが、PrismaなどのORMを利用することも可能です

https://nextjs.org/learn/dashboard-app/fetching-data

第8章 静的レンダリングと動的レンダリング

  • この章では、Next.jsを利用した静的レンダリングと動的レンダリングについて学べます
  • 静的レンダリングとは、フェッチしたデータをビルド時に予め取り込み、静的なHTMLとすることです。
  • 静的レンダリングは、サーバサイドのデータ取得処理やAjaxなどが必要ないことと併せて、CDNのキャッシュを利用した配布ができることでWebサイトの高速化、サーバ負荷の軽減、SEOへの配慮がメリットとしてあげられます
  • ただし、更新頻度が高い動的なデータの場合、静的レンダリングは向きません
  • リアルタイムなデータのレンダリングを行う場合、動的レンダリングの出番です
  • ただし、動的レンダリングの場合、データの取得に時間がかかってしまうと、ページ全体のレンダリングブロックが発生します
  • 動的レンダリングを使用すると、アプリケーションは最も遅いデータフェッチと同じ速度でしか動作しません

https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering

第9章 ストリーミング

  • この章では、ストリーミングについて学べます
  • データ要求が遅い場合でもUXを向上させる方法としてストリーミングという方法があります
  • ストリーミングとは、大きいデータ取得を小さなチャンクに分割し、サーバから段階的にダウンロードするデータ転送技術です
  • ストリーミングのおかげで、遅いデータによるページ全体のレンダリングブロックを防ぐことができ、データがすべて読み込まれる前に、ユーザーに対して必要なUIを表示することができます
  • 各コンポーネントはチャンクとみなすことができる為、ストリーミングは各コンポーネントのモデルと連携しやすいです
  • Next.jsでストリーミングを実装する方法は2つあります
    • 各ページレベルでloading.tsxを準備する方法
    • データフェッチが発生するコンポーネントで、React Suspenseを利用する方法
  • 各ページレベルでloading.tsxを準備する方法では、下記のような内容のloading.tsxを作成することで、初回のデータをフェッチしている間、画面上に「Loading...」という内容を表示することができます
export default function Loading() {
  return <div>Loading...</div>;
}
  • この方法を利用する場合、データ読み込まれる前のスケルトンデザインを表示することで、ユーザーに対してどんな内容の要素が読み込まれるのかを伝えられるため、読み込み遅延に対するストレスの軽減に繋がります
  • React Suspenseを利用する方法では、コンポーネント単位でローディングUIを表示する実装ができます
  • 下記のように記述することで、RevenueChart内のデータフェッチが完了するまでは、RevenueChartSkeletonコンポーネント表示をすることができ、ページ全体のレンダリングブロックを防ぐことができます
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';

...

<Suspense fallback={<RevenueChartSkeleton />}>
  <RevenueChart />
</Suspense>

https://nextjs.org/learn/dashboard-app/streaming

第10章 部分的プリレンダリング(PPR) ※オプション

  • この章では、Next14で導入された実験的機能のPPRを学べます
  • 現在のWeb開発においては、アプリケーション全体もしくは特定のページにおいて、動的レンダリングか静的レンダリングかのどちらか一方を選択することが主流です
  • しかし、ほとんどのページにおいて、完全な静的または動的の状態ではなく、どちらもが混在している状態である場合が多いです
  • Next14のPPRでは、一部を動的に保ちつつ、ページ全体を静的な読み込みで提供します
  • これにより、初期ロードは高速になり、非同期部分は並行してストリーミングされる為、ページ全体の読み込み時間が短縮されます
    - PPRは、超高速な静的エッジ配信と動的レンダリングの長所を組み合わせたもので、Webアプリケーションのデフォルトのレンダリングモデルになる可能性があると考えられています
  • 参考:https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model

https://nextjs.org/learn/dashboard-app/partial-prerendering

第11章 検索とページネーションの追加

  • この章では、Next.js APIのuseSearchParams、usePathname、useRouterを学べます
  • useSearchParams → /dashboard/invoices?page=1&query=pending の様なURLから {page: '1', query: 'pending'} といった形でパラメータを取得可能です
  • usePathname → 現状の表示しているパス名を取得可能です
  • useRouter → クライアントコンポーネント上で使用することができ、パスやクエリの書き換えを簡易的に行うことができます
  • サンプルコードを変更することで、それぞれのAPIを学ぶことができますが、ここでは割愛します
  • デバウンス制御で利用する useDebouncedCallback は初めて知ったので、有効活用していこうと思います

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

第12章 データの変更

  • この章では、React Server Actions を利用したCRUD実装を学べます
  • React Server Actionsを利用すると、サーバー上で非同期関数を直接実行することができるため、APIエンドポイントは作成する必要がなくなります
  • サーバーアクションをフォームで使用した場合、下記のようなコードとなり、通常のReactのコードを記述する感覚でサーバー上のデータを更新可能です
// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    'use server';
 
    // Logic to mutate data...
  }
 
  // Invoke the action using the "action" attribute
  return <form action={create}>...</form>;
}
  • サーバーアクションを作成する場合、そのファイルの先頭に 'use server;' を追加します
  • サーバーコンポーネント内にuse serverを記述することもできますが、サーバーアクションを作成したいときは上記の方法を利用します
  • サーバーコンポーネントのサンプルは以下です(公式のサンプルコードをそのまま引用)
import { z } from 'zod';
import { sql } from '@vercel/postgres';
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}
  • やっていることは、PHPでやっていた開発をJSだけでできると言った感覚です
  • ただし、Reactコンポーネントで、APIエンドポイントをもとにしたデータフェッチのコードを書くよりもかなり体験は良いです
  • ここでは、更新、削除のそれぞれのコードは割愛させていただきます

https://nextjs.org/learn/dashboard-app/mutating-data

第13章 エラーの処理

  • この章では、Next.js上で、サーバーアクションのコードをエラーハンドリングする方法が学べます
  • 基本的には、サーバアクション上で、DBとの通信部分に対してtry catchを適用する形になります
  • 以下は削除のコードの引用です
export async function deleteInvoice(id: string) {
  try {
    await sql`DELETE FROM invoices WHERE id = ${id}`;
    revalidatePath('/dashboard/invoices');
    return { message: 'Deleted Invoice.' };
  } catch (error) {
    return { message: 'Database Error: Failed to Delete Invoice.' };
  }
}
  • ディレクトリ内に error.tsx というファイルが存在すると、エラーが発生した際に、そこのファイルの内容がブラウザに表示されます
  • 404エラーもtry catchでハンドリングすることができますが、not-found.tsx というファイルが存在すると、404エラーが発生した際、このファイルの内容がブラウザに表示されます

https://nextjs.org/learn/dashboard-app/error-handling

第14章 アクセシビリティの向上

  • この章では、eslint-plugin-jsx-a11y を利用したアクセシビリティのベストプラクティスを学ぶことが可能です
  • アクセシビリティとは、障害のある人を含む誰もが、Webアプリケーションを使用できる様な設計、実装を指します
  • キーボードナビゲーション、セマンティックHTML、画像、色、ビデオなど多くの領域をカバーする広大なトピックです
  • Next.jsには、eslint-plugin-jsx-a11y というアクセシビリティの問題を早期に発見することができるプラグインが含まれています
  • "lint": "next lint" をpackage.jsonのscriptに追加し、コマンド実行することで、各コンポーネントのアクセシビリティチェックが可能です
  • 例えば、Imageコンポーネントからalt属性を削除すると、アクセシビリティチェックのエラー対象になります
  • この章ではフォームのアクセシビリティ向上も取り扱っています
  • useFormState, zod, 各種WAI-ARIAのプロパティサンプルが提示されており、エラーメッセージに対してアクセシビリティ的にどういった記述をすればよいかが学べます
  • 以下は、WAI-ARIAのプロパティ対応の引用コードです
<form action={dispatch}>
  <div className="rounded-md bg-gray-50 p-4 md:p-6">
    {/* Customer Name */}
    <div className="mb-4">
      <label htmlFor="customer" className="mb-2 block text-sm font-medium">
        Choose customer
      </label>
      <div className="relative">
        <select
          id="customer"
          name="customerId"
          className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
          defaultValue=""
          aria-describedby="customer-error"
        >
          <option value="" disabled>
            Select a customer
          </option>
          {customers.map((name) => (
            <option key={name.id} value={name.id}>
              {name.name}
            </option>
          ))}
        </select>
        <UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
      </div>
      <div id="customer-error" aria-live="polite" aria-atomic="true">
        {state.errors?.customerId &&
          state.errors.customerId.map((error: string) => (
            <p className="mt-2 text-sm text-red-500" key={error}>
              {error}
            </p>
          ))}
      </div>
    </div>
    // ...
  </div>
</form>
  • aria-describedby は、エラー表示要素に対するIDとの紐づけです
  • aria-live は、スクリーンリーダーなどで読み上げる際の設定なのですが、現在の読み上げ終了時、適切なユーザーの入力タイミングに応じて、更新内容はユーザーに伝達します

https://nextjs.org/learn/dashboard-app/improving-accessibility

第15章 認証の追加

  • この章では、NextAuthを利用した認証機能の追加を学べます
  • NextAuthはアプリケーションに認証を追加するためのライブラリで、セッション、サインイン、サインアウト周りの多くの複雑なプロセスを簡易的に扱うことが可能です
  • 以下でインストールを行います
npm install next-auth@beta
  • 以下で、アプリケーションの秘密鍵を作成します
openssl rand -base64 32
  • .env に生成した秘密鍵を記述し、併せてVercel側の記述も変更します
AUTH_SECRET=your-secret-key
  • /auth.config.ts を利用することで、サインイン、サインアウト、エラーページのルートページを指定することが可能です。
  • 以下の引用コードの場合は、デフォルトのpage.tsxではなく、/loginのpage.tsxの内容が表示されることになります
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
};
  • 以下の様な引用のコードを記述することで、 /dashboard のURLに対しては、認証を通過しない限りアクセスできないようになります
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
  • middlewareを適用したい場合は、middleware.tsに設定を記述することにより対応ができます
  • 例えば、サンプルの引用コードは、 api|_next/static|_next/image|.*\\.png といったパス、ファイル名称に一致しないものに対してだけmiddlewareを適用する様なコードになっています
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export default NextAuth(authConfig).auth;
 
export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
  • サンプルコードでは引き続き、auth.tsというファイルを作成し、NextAuthに対するサインイン、サインアウトの設定を行っています
  • ここでは主にDBに格納されているID、パスワードとの照合処理をNextAuthのAPIであるCredentialsをインポートして利用している様です
  • 暗号化されたパスワードの照合は、bcryptを利用するなどの処理がなされています
  • 正直、この章は自分がバックエンドの認証周辺の知識が明るくないので、コードを写経をするだけで、他との比較ができていないと感じています
  • コードを追っている限りは、NextAuthをはじめ各種ライブラリのおかげで、認証系が単純な関数利用と比較処理になっているので、わかりやすいとは感じています

https://nextjs.org/learn/dashboard-app/adding-authentication

第16章 メタデータの追加

  • この章では、Next.jsを利用したmetaデータの追加方法が学べます
  • metaデータは皆さんおなじみのheadタグに記述するtitle, meta nameやOGPの設定などのことを指します
  • Next.jsでは、layout.tsxに対して Next.js に標準装備されている Metadata をインポートすることで簡単に指定ができます
  • 以下は引用コードです
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Acme Dashboard',
  description: 'The official Next.js Course Dashboard, built with App Router.',
  metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};
 
export default function RootLayout() {
  // ...
}
  • layout.tsxにMetadataの設定を記述することで、ページ全体に設定を利用することが可能です
  • 各種ページで個別のMetadataを設定したい場合は、各ディレクトリに存在するpage.tsxに同じ様なMetadataの記述を行うことで、必要な箇所の上書きが可能です
  • 以下は引用コードです
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Invoices | Acme Dashboard',
};
  • 以下の引用コードのように %s をlayout.tsxで指定し、各page.tsxで部分的な上書きを行うことも可能です
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: {
    template: '%s | Acme Dashboard',
    default: 'Acme Dashboard',
  },
  description: 'The official Next.js Learn Dashboard built with App Router.',
  metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};

https://nextjs.org/learn/dashboard-app/adding-metadata

まとめ

以上で、まとめ終わりです。
かなり長くなってしまい、申し訳ありません。
これまでの Next.js は、雰囲気で扱えるほどシンプルでしたが、ディレクトリの分け方やコンポーネント、ロジックの管理方法は、利用者によって差が生まれてしまうほどの自由度があるように感じていました。
Learn Next.js で感じた点は、Next.js がさらに扱いやすく、昔よりも利用者によってあまり差が生まれないような作りにできるフレームワークに成長しているということです。
また、PPRやAppRouterの様な、Webの体験を最大化し、ユーザーにとってのうれしさを提供するだけでなく、われわれWebエンジニアにとっても、開発体験をワクワクさせる機能が備わっているところが良い点だなと感じています。
まだ、個人的なNext.js14の案件利用ができておらず、X界隈を見る限り、AppRouterに対してネガティブな声があるのも散見されるので、今後案件利用し、本来の力を試せればと思います。
本記事を最後までお読みいただきありがとうございました。

ランサーズ株式会社

Discussion