2024-3I プログラミング3 第06回 講義資料

2024年11月14日(木)1・2時限

1 連絡

2 バックエンド処理を含めたブログアプリの開発

ここまでの授業では、フロントエンドのみで完結する Todoアプリの開発 を通じて、「React」を使ったモダンウェブ開発の基礎について学んできました。ここからの授業では Next.js フレームワークとして使用し バックエンド (サーバサイド) を含めた「本格的なモダンウェブアプリ開発」 について体験的に学んでいきます。

現代のウェブアプリは「❶ ウェブブラウザで実行されるフロントエンド部分」と「❷ サーバ上で実行されるバックエンド部分」を RESTfulなウェブAPI (主にHTTP/HTTPSプロトコル上で実装) を介して「疎結合」な形で構成することが一般的となっています。そして、この設計によってフロントエンドとバックエンドで 異なる「プログラミング言語」や「フレームワーク」を使用すること が可能になっています。

フロントエンドの開発言語 は実質的に HTML/CSS/JavaScript (TypeScript) に限定されており、ReactVue.jsSvelte などのコンポーネント指向フレームワークが主流となっています。かつて広く使用されていた jQuery は、これらのモダンフレームワークの登場と普及によって、新規開発案件での採用は減少傾向 にあります。

一方、バックエンドにおいては、次のような多様な「プログラミング言語」と「フレームワーク」の組み合わせの選択肢があり、実際に幅広く活用されています。

  1.  Ruby (Ruby on Rails)
  2.  PHP (Laravel)
  3.  Java (Spring Boot)
  4.  Python (Django、FastAPI、Flask)
  5.  C# (.NET ASP Core)
  6.  Rust (Actix)
  7.  Go (Gin、Echo)
  8.  JavaScript/TypeScript (Express、Fastify)

このような状況のなか「Next.js」は、TypeScript (JavaScript) を用いて フロントエンドとバックエンドを単一のプロジェクトとして統合的に開発できるという異色 ( 革新的?! ) のフレームワーク として人気を集めています。Next.js のフロントエンドは Reactをベースとしているため (ただしルーティングなどの一部機能を除く)、Todoアプリ開発で学んだ React のスキルと経験をそのまま活かして学ぶことができます。

今回の授業からは、この Next.js を使用して「ブログアプリ」の開発に取り組んでいきます(チュートリアル的に「ブログアプリ」の開発を進めていきます)。最終的にはSupabaseという Baas (Backend As A Service) を活用し、ブログ記事の CRUD操作 ( Create/Read/Update/Delete )、ログイン機能 (ユーザ認証機能) までを実装していきます。

ブログアプリの完成後は、最終課題 として「オリジナルのウェブアプリを開発」に取り組んでもらいます。

大まかな流れは、次のようになっています。

  1. Next.js 開発環境の構築 ← 今回の授業
  2. ブログアプリの「フロントエンド開発」のチュートリアル ← 今回の授業
  3. ブログアプリの「バックエンド開発」のチュートリアル
  4. ブログアプリのカスタマイズとつくり込み → 課題2
  5. オリジナルのウェブアプリ → 課題3 (最終課題)

2.0.1 定着確認

3 Next.js の 開発環境構築

Next.js は 2024年10月21日に最新バージョンの「15」がリリースされています。しかしながら、2024年11月1日現在、react-hook-form などの主要な周辺ライブラリが、まだ Next.js 15 に対応していない状況となっています。そのため、本授業では安定版の Next.js 14 を使用していきます。

以下、プロジェクト名称を「next-blog-app」として、Next.js 14 の開発環境を構築していきます。

ターミナル (PowerShell) を起動し、プロジェクトフォルダを作成したい位置 (例えば C:\Users\xxxx\Documents\ など) に移動してください。Next.js の開発環境は、次のコマンドを使って構築していきます。

npx create-next-app@14

ここで npm ではなく npx とする点に注意してください (違いが気になる人は生成AIで解決してください)。また、最新バージョン である「15」で環境構築したい場合は npx create-next-app@latest としてください。

(プロンプト例)

Node.js における npmnpx の違いを初心者向けに分かりやすく解説してください。

 

はじめて npx create-next-app を実行するときや、時間を空けて実行するときは、以下のように訊かれるので y で回答してください。

Need to install the following packages:
create-next-app@14.2.16
Ok to proceed? (y)

create-next-app では、開発環境に関していくつかの質問がされます。ここでは、以下のように回答 (設定) してください。ここで誤った設定をしてしまうと、あとからの修正が大変になるため、慎重に設定してください。

√ What is your project named? … next-blog-app (プロジェクトの名称にあわせてせ設定)
√ Would you like to use TypeScript? … No / Yes
√ Would you like to use ESLint? … No / Yes
√ Would you like to use Tailwind CSS? … No / Yes
√ Would you like to use src/ directory? … No / Yes
√ Would you like to use App Router? (recommended) … No / Yes
√ Would you like to customize the default import alias (@/*)? … No / Yes
√ What import alias would you like configured? … @/*

最後の「What import alias would you like configured?」では、そのまま Enter を押下すれば、上記のように @/* が設定されます。全ての質問に答えると各種パッケージのダウンロードとインストールを含めた環境構築が開始されます (しばらく時間がかかります)。

最終的に以下のように Success! Created next-blog-app のようなログが表示されれば成功です。

Success! Created next-blog-app at C:\Users\xxxx\Documents\next-blog-app

A new version of `create-next-app` is available!
You can update by running: npm i -g create-next-app

なお、設定を間違えてしまったときは、エクスプローラから当該フォルダを削除してから、再度 npx create-next-app@14 を実行してください。

3.0.1 定着確認

3.1 VSCodeの起動

作成されたプロジェクトフォルダ (next-blog-app) に移動して VSCode を起動してください。

cd next-blog-app
code .

プロジェクトは、以下のようなフォルダ構成になっているはずです。

img

プロジェクトフォルダを構成しているファイルについて確認していきます。

3.2 .eslintrc.json

.eslintrc.json は、ESLint (前回講義で解説済み) のための設定ファイルです。Vite で構築したReact環境ではeslint.config.jsという「JSファイル」に設定を記述しましたが、ここでは「JSONファイル」に設定を記述します。

(プロンプト例)

ESLintの設定ファイルとして .eslintrc.jsoneslint.config.js の2つの形式が存在するのは何故ですか?

3.3 .gitignore

Gitによる バージョン管理をしないファイルやフォルダ (GitHubで公開しないファイルやフォルダ) を設定するファイルです。ライブラリをインストールする /node_modules フォルダなどが除外設定として記入されています。

このプロジェクトでは、.env という環境変数の設定ファイル (= あとで「microCMSのAPIキー」や「supabseのDB接続文字列」を記述する予定のファイル) もバージョン管理から除外したいので、次のように追記してください。

# local env files
.env
.env*.local

3.4 next-env.d.ts

TypeScriptが Next.js の「組み込み機能」や「型」を認識するためのグローバル型宣言ファイルです。ファイルに書かれているように This file should not be edited です。

3.5 next.config.mjs

next.config.mjs では 外部ドメインからの画像読み込みの制御 に関する設定などを記述します。必要に応じて設定を追加したり、書き換えたりする必要があります。

3.6 package.json、package-lock.json

これらのファイルについては、既に解説済みです。package.json"scripts" に記載の各コマンドは次のように動作します。

3.6.1 npm run dev

ホットリロード」や「エラー表示」の機能を提供する 開発サーバ でアプリを起動します。デフォルトでは3000番ポートで起動します。実行時に プロジェクトに「.next」というフォルダ が生成され、開発用のビルドファイルが出力されます。このフォルダは .gitignore のなかでバージョン管理の対象外に設定されています。

VSCodeのターミナル (Ctrl+Jで起動) から実際に以下のコマンドを実行してみてください。

npm run dev

起動したサーバは http://localhost:3000/ でアクセス可能で、ターミナルから Ctrl+C を入力すると停止可能です。

img

トップページ (http://localhost:3000/) の画面は、Next.js のバージョンによっては違ったものになる可能性があります。画面上の説明も簡単に把握しておいてください。

  1. Get started by editing src/app/page.tsx. (まずは src/app/page.tsx を編集することから始めましょう)
  2. Save and see your changes instantly. (編集して保存すると、変更が直ちに画面に反映されます)

3.6.2 npm run build

本番用 (プロダクション環境用) にアプリをビルドします。コードの「最適化」「バンドル」「ミニファイ化」などが行われたファイルが .next フォルダに出力されます (ビルドをするだけでサーバは起動しません)。デプロイをする前には、必ず実行する必要があります。

実際に実行してみてください。また、ビルドについては生成AIを使って概要を把握してください。

(プロンプト例)

Node.js + TypeScript環境のウェブアプリ開発において、ビルドプロセスで実行される「最適化」「バンドル」「ミニファイ化」とは、どのような処理ですか。

3.6.3 npm run start

ビルドされた本番用のアプリを起動します。開発サーバと同様にデフォルトでは3000番ポートで起動します。npm start でも実行可能です。

実際に実行してみてください。

3.6.4 npm run lint

ESLint による静的コード解析 (コード品質チェック) を実行します。このコマンドの実行結果が ✔ No ESLint warnings or errors であれば、特に問題が検出されなかったということになります。

実際に実行してみてください。

なお、VSCode に適切な拡張機能をインストールしている場合は、リアルタイムにエディタ上に「警告」や「エラー」が表示されるので、基本的には、このコマンドを使用することはありません。

img

3.7 tailwind.config.ts、postcss.config.mjs

Tailwind CSS に関する設定ファイルです。Tailwind CSS に関連するライブラリを追加したときは、設定の追記が必要となることがあります。

3.8 tsconfig.json

TypeScript に関する設定ファイルです。必要に応じて編集することがあります。

3.9 README.md

就職・インターンシップ向けのポートフォリオ用として開発・公開するときは、README.md の内容を適切に書き換えてください。

(プロンプト例)

就職・インターンシップ向けのポートフォリオ用としてウェブアプリを開発しました。このリポジトリを公開するにあたって、README.md は、どのような構成にして、どのような内容を記載すると良いでしょうか。採用担当者の視点から解説してください。

逆にマイナスの評価や印象となるような README.md とは、どのようなものでしょうか。具体例を示しながら解説してください。

3.10 src/app フォルダ

Next.js では、基本的に src/app のなかにファイルやフォルダを配置することでコンテンツを構成していきます。この際、ファイルの「名前」や「位置 (階層構造)」が、ファイルに記述されるコードの役割や機能を直接的に決定づける点 に特徴があります。この設計思想は 設定より規約(Convention over Configuration: CoC)と呼ばれるアプローチに基づくものになっています。

Next.js における「設定より規約」の最も特徴的なものとして URLルーティング が挙げられます。

ウェブアプリにおけるルーティングとは、URLを構成するパス (例えば /posts/1/about) に応じて 「どのコンテンツを表示するか」を決める仕組み を指します。

3.10.1 Next.js のルーティング

Next.js ( バージョン13から導入された AppRouter ) では、src/app のなかに規約に従った名前でファイルを配置 (基本的には layout.tsx もしくは page.tsx)、フォルダ階層を構成することで (自動的に) ルーティングが設定され、フォルダ構成がそのままURLのパスとなります。

例えば、次のようにプロジェクトフォルダを構成すると…

📂 src/app/
├─ page.tsx    # / のコンテンツを定義
├─ layout.tsx  # 全てのページで共有されるレイアウトを定義
└─ 📂 foo/
    └─ page.tsx  # /foo のコンテンツを定義

次のようなルーティングルールが自動的に適用されます。

また、layout.tsx入れ子構造 で適用され、親から子に順番にレイアウトが重ねられていきます。例えば、次のようにプロジェクトフォルダを構成して http://xxxx:3000/bar/baz をリクエストすると、、、

📂 src/app/
├─ page.tsx
├─ layout.tsx  # 全てのページで共有されるレイアウトを定義
└─ 📂 bar/
    ├─ page.tsx
    ├─ layout.tsx  # /bar 階層下のページで共有されるレイアウトを定義
    └─ 📂 baz/
        └─ page.tsx  # /bar/baz のコンテンツを定義

次のように複数の layout.tsx が順番に適用されます。

  1. src/app/layout.tsx のなかに、
  2. src/app/bar/layout.tsx が配置され、
  3. そのなか src/app/bar/baz/page.tsx のコンテンツが表示されます。
img

App Router と Pages Router

Next.js のバージョン「12」以前では「Pages Router」というルーティングシステムが採用されていました。現在の Next.js(最新バージョンは「15」)では、2022年10月のバージョン13.0で導入され、2023年5月の13.4で安定版となった「App Router」という 新しいルーティングシステムの利用が推奨 されています。

ただし、ウェブ上の記事や生成AIの回答では、まだ Pages Router を前提とした情報が多く見られます。ウェブ検索や生成AIを利用する場合は、この点に留意してください。特に、生成AIでは 「Next.js (v14 + TypeScript + App Router) に関する質問です。」 のような文をプロンプトに含めるようにしてください。

3.10.2 定着確認

4 最小構成の Next.js アプリ

まずは、最低限必要なファイルだけで、最もシンプルにアプリを構成していきます。プロジェクトフォルダの src/app に対して、以下の作業を行なってください。

4.1 fonts フォルダの削除

fontsフォルダを削除してください。

4.2 global.css の編集

global.css を次のように編集してください (先頭の3行を残して以降の内容を全て削除)。ファイルの編集後は忘れずに保存操作をしてください、以下同様。

@tailwind base;
@tailwind components;
@tailwind utilities;

なお、VSCode上で @tailwind に「警告」もしくは「エラー」の波線が表示されるかもしれませんが、問題ありません (Tailwind CSS 関連のLint設定の項目で解決します)。

4.3 page.tsx の編集

先頭の "use client"; は重要な意味を持ちます。この講義では、インタラクティブなウェブアプリを作成するため、原則として全てのTSXファイルsrc/app/layout.tsx を除く)の先頭には、この宣言を入れてください。これにより、その関数コンポーネントで Reactの状態管理useState や useEffect などの ReactHooks)が使用できるようになります。

page.tsx を次のように書き換えてください。

"use client";

const Page: React.FC = () => {
  return (
    <main>
      <div className="font-bold text-2xl">Main</div>
    </main>
  );
};

export default Page;

ここで 第03行目Page は、アロー関数形式で記述した Reactの関数コンポーネント です。JSX (JavaScript XML) が戻り値になっていることを確認してください。なお、型 React.FC の FCは Function Component (関数コンポーネント) の略になります。

関数コンポーネントの名前は自由に設定できますが、先頭を大文字にしてパスカルケースで命名する必要があります (例えば page のように先頭を小文字にすると予期せぬトラブルの原因となります)。また、page.tsx に記述する関数コンポーネントには、第11行目のように必ず export default キーワードを付けてデフォルトエクスポートに設定してください。

なお、型推論が働くので、第03行目React.FC は省略も可能であり、さらに function形式 にすれば export default を定義部に直接的に記述可能なので、次のように書くこともできます。ただし、どちらの形式で書かれていても読解できるようになっておいてください

"use client";

export default function Page() {
  return (
    <main>
      <div className="font-bold text-2xl">Main</div>
    </main>
  );
}

次項で説明しますが、同じ階層 (フォルダ内) に layout.tsxpage.tsx が存在するときは、layout.tsx のなかの {children}page.tsx が埋め込まれてレンダリングされます。

4.3.1 定着確認

「リスト1」は、Next.js のプロジェクトに配置したファイルである。以下の問いに答えよ。


const Page: React.FC = () => {
  return (
    <main>
      <div className="font-bold text-2xl">Main</div>
    </main>
  );
};
 

4.4 layout.tsx の編集

Next.js において src/app/layout.tsxアプリのレイアウトに関するエントリーポイント (最初に読み込まれてDOMツリーを構築する基点となるファイル) という位置づけになります。そして、このファイルには、アプリ全体のHTML構造を定義した関数コンポーネントを記述します。

この page.tsx に記述する関数コンポーネントと同じく 任意の名前を設定可能 ですが、次の条件を満たす必要があります。

  1. export default を付与してデフォルトエクスポートに設定すること (第27行目)。
  2. Prosp として children プロパティが受け取れること (第13行目)。
  3. "use client";記述しないこと
    • ただし src/app 直下以外に配置する layout.tsx には "use client"; を記述してください。

layout.tsx を次のように書き換えてください。

import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "NextBlogApp",
  description: "Built to learn Next.js and modern web development.",
};

type Props = {
  children: React.ReactNode;
};

const RootLayout: React.FC<Props> = (props) => {
  const { children } = props;
  return (
    <html lang="ja">
      <body>
        <header>
          <div className="bg-slate-800 text-white font-bold py-2">Header</div>
        </header>
        <div>{children}</div>
      </body>
    </html>
  );
};

export default RootLayout;

Prosp として受け取る children には URLのパスに応じて取得されるReact関数コンポーネント が代入されます。例えば、http://xxxx/foo をリクエストしたときには、プロジェクトフォルダの src/app/foo/page.tsx のなかでデフォルトエクスポートされる関数コンポーネントが children に読み込まれます。

第04行目 から 第07行目 で設定した内容は、最終的にレンダリングされるHTMLの <head>...</head> に反映されます。

img

4.5 動作確認

src/app は、次のようなファイル構成となるはずです。

img

npm run dev で開発モードでアプリを起動して http://localhost:3000/ にアクセスしてください。次のような画面が表示されるはずです。

img

4.6 Tailwind CSS 関連のLint設定

Tailwind のユーティリティクラスの自動並べ替えを行なうための設定を行ないます。基本的にはReact開発環境で設定した内容と同じです。

VSCodeの拡張機能としての Prettier (識別子:esbenp.prettier-vscode) とESLint (識別子:dbaeumer.vscode-eslint) はあらかじめインストールされているものとします。

VSCode でターミナルを起動し、次のように「prettier」と「eslint-plugin-tailwindcss」を開発用のパッケージとしてインストールします。

npm i -D prettier eslint-plugin-tailwindcss

つづいて .eslintrc.json を次のように変更してください。

{
  "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
  "settings": {
    "tailwindcss": {
      "callees": ["cn", "twMerge", "tv"]
    }
  }
}

さらに、プロジェクトフォルダの「直下」.vscode フォルダを作成して、そのなかに settings.json を作成して次の内容を記述してください。

{
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.formatOnType": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": ["source.fixAll.eslint"],
  "css.lint.unknownAtRules": "ignore"
}

設定ファイル名は setting.json ではなく settings.json なので注意してください。

なお、最後の "css.lint.unknownAtRules": "ignore" の設定によって src/app/globals.css で表示されていたエラーが抑制されます。

src/app/layout.tsx および src/app/page.tsx を開くと、JSXのclassName属性に警告 (Invalid Tailwind CSS classnames order) の黄色波線が表示され、その状態で保存操作をすると並び替えが実行されます。

5 ヘッダのコンポーネント化

Next.jsでは、Next.jsでは 再利用性保守性 を高めるために、コードの「コンポーネント化」が推奨されています。コンポーネント化とは UIやロジックの共通部分を再利用可能な小さなパーツとして切り出すこと を意味します。

ここでは例として、src/app/layout.ts第18行目 から 第20行目 に記述している「ヘッダ部」を 「Header」というコンポーネントに切り出して別ファイルで管理する手順を説明します。

const RootLayout: React.FC<Props> = (props) => {
  const { children } = props;
  return (
    <html lang="ja">
      <body>
        <header> {/* ここから */}
          <div className="bg-slate-800 text-white font-bold py-2">Header</div>
        </header> {/* ここまでをHeaderコンポーネント化する */}
        <div>{children}</div>
      </body>
    </html>
  );
};

まずは src/app_components というフォルダを作成して、そこに Header.tsx を新規作成してください。

ここでは、フォルダ名の先頭に必ず アンダーバー _ をつけるようにしてください。もし、components とすると、それは http://localhost:3000/components のようなルーティングの対象として機能してしまいます。一方で、先頭にアンダーバーをつけるとルーティングの対象外となり、純粋にコンポーネント用のフォルダとして扱われるようになります。

img

つづいて Header.tsx に以下のように Header という関数コンポーネントを記述して保存してください。なお、慣例的に Reactの関数コンポーネントを記述するファイル名は、先頭を大文字にしたパスカルケース (例えば Header.tsxButton.tsx など) で名前を付けます。ただし、Next.js の特別なファイルである page.tsxlayout.tsx は例外的に先頭を小文字にします。

"use client";

const Header: React.FC = () => {
  return (
    <header>
      <div className="bg-slate-800 py-2 font-bold text-white">Header</div>
    </header>
  );
};

export default Header;

次に src/app/layout.tsx を次のように書き換えてください。第03行目第19行目 が書き換えられた部分になります。

import type { Metadata } from "next";
import "./globals.css";
import Header from "@/app/_components/Header";

export const metadata: Metadata = {
  title: "NextBlogApp",
  description: "Built to learn Next.js and modern web development.",
};

type Props = {
  children: React.ReactNode;
};

const RootLayout: React.FC<Props> = (props) => {
  const { children } = props;
  return (
    <html lang="ja">
      <body>
        <Header />
        <div>{children}</div>
      </body>
    </html>
  );
};

export default RootLayout;

第03行目 では src/app/_components/Header.tsx で定義している Header コンポーネントをインポートしています。パスの先頭を @ にすると src をルートとした 絶対パス でファイル位置の指定が可能になります。これは、Next.js の環境構築で以下のように設定しているためです。

√ Would you like to customize the default import alias (@/*)? … No / Yes
√ What import alias would you like configured? … @/*

なお、import Header from "./_components/Header" のように相対パスでも指定が可能です。プロジェクトフォルダが複雑になってくるほど、絶対パスで指定できることのメリットが大きくなります。

以上のようにインポートしたHeaderコンポーネントを、第19行目 において <Header /> のような自己完結タグで配置しています。これは <Header></Header> のように記述することもできます。

開発モードでブログアプリを実行して (npm run devを実行して)、ヘッダ部をコンポーネント化する前と同様にアプリが表示されること確認してください。

img

5.1 FontAwesome の利用

Next.js でFontAwesomeの各種アイコンを使用する場合は少し注意が必要になってきます。

まずは、同様にパッケージをインストールしてください。

npm i @fortawesome/react-fontawesome @fortawesome/fontawesome-svg-core
npm i @fortawesome/free-solid-svg-icons

また、src/app/layout.tsx を以下のように編集します (第04行目から第06行目の内容を記述してください。この部分が Next.js 固有の注意点になります。

import type { Metadata } from "next";
import "./globals.css";

import "@fortawesome/fontawesome-svg-core/styles.css";
import { config } from "@fortawesome/fontawesome-svg-core";
config.autoAddCss = false;

import Header from "@/app/_components/Header";

export const metadata: Metadata = { ...
// 以下、略

つづいて、実際にアイコンを使いたいコンポーネントでの処理になります。ここでは、Headerコンポーネントにサカナのアイコン() を追加していきたいと思います。

src/app/_components/Header.tsx を次のように書き換えてください。第03行目第09行目 が編集部分になります。

"use client";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFish } from "@fortawesome/free-solid-svg-icons";

const Header: React.FC = () => {
  return (
    <header>
      <div className="bg-slate-800 py-2 font-bold text-white">
        <FontAwesomeIcon icon={faFish} className="mr-1" />
        Header
      </div>
    </header>
  );
};

export default Header;

各ファイルが保存されていることを確認して、アプリを実行して表示を確認してください。次のようにヘッダになアイコンがついているはずです。

img

なお、layout.tsx に追加した3行をコメントアウトすると (リロードしたときに) 表示が乱れることも確認してください。

5.2 レイアウトの調整

現状では、左端に余白がなくレスポンシブデザインにもなっていないので、ここでレイアウトについての大まかな調整していきます。次の図のようなレイアウトになるように設定していきます。

img

まずは、ReactによるTodoアプリ開発でも使用した「tailwind-merge」をインストールしてください。

npm i -D tailwind-merge

次に src/app/_components/Header.tsx を次のように編集してください。第19行目 の「Header」の文字列は「XXX’s Tech Notes」や「XXXのブログ」のように適当に書き換えてください。

"use client";
import { twMerge } from "tailwind-merge";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFish } from "@fortawesome/free-solid-svg-icons";

const Header: React.FC = () => {
  return (
    <header>
      <div className="bg-slate-800 py-2">
        <div
          className={twMerge(
            "mx-4 max-w-2xl md:mx-auto",
            "flex items-center justify-between",
            "text-lg font-bold text-white"
          )}
        >
          <div>
            <FontAwesomeIcon icon={faFish} className="mr-1" />
            Header
          </div>
          <div>About</div>
        </div>
      </div>
    </header>
  );
};

export default Header;

上記は、主に 第09行目 から 第16行目 に編集を加えています。ここでは解説と可読性・保守性の都合で twMerge を使ってクラスを3分割して記述していますが、実際には twMerge を使わずに className="..." として全てのクラスを列挙可能です。

まず、第12行目 は、max-w-2x によって div要素の最大幅 を 2xl (=672px) に設定しています。また、mx-4 によって X方向 (左右) のマージン を 16px に設定しています。また、md:mx-auto によって画面幅が md (=768px) を超えるときに左右のマージンを自動に設定して、要素を水平方向に中央揃えにするようにしています。

第13行目 は「 Header」と「About」を 左右の両端揃え かつ 上下中央揃え にするための設定になります。

第14行目 は「 Header」と「About」の フォントスタイルに関する設定 になります。

Tailwind CSS のクラスの詳細については、生成AIを利用して学んでください。

(プロンプト例)

Tailwind CSS を使用している環境で、div要素に “flex items-center justify-between” というクラスが設定されていました。これらのクラスはどのようなレイアウト設定か解説してください。


次に src/app/layout.tsx を次のように編集してください。

const RootLayout: React.FC<Props> = (props) => {
  const { children } = props;
  return (
    <html lang="ja">
      <body>
        <Header />
        <div className="mx-4 mt-2 max-w-2xl md:mx-auto">{children}</div>
      </body>
    </html>
  );
};

開発モードでブログアプリを実行して、次のようなレイアウトになっていることを確認してください。

img

また、Chrome のデベロッパーツールから、モバイル表示に切り替えてレイアウトを確認してください。

img

5.2.1 演習 ( 10分)

Header.tsx を次のように変更すると、どのようにレンダリングされるか確認してください。

(変更前)

<div>
  <FontAwesomeIcon icon={faFish} className="mr-1" />
  Header
</div>
<div>About</div>

(変更後)

<div>
  <FontAwesomeIcon icon={faFish} className="mr-1" />
  Header
</div>
<div>検索</div>  {/* 追加 */}
<div>About</div>

さらに、次のようなレイアウトのヘッダとするためには、どのようにすればよいか考えて実装してみてください。

img

上記レイアウトの実装例

変更後は、一旦、元の状態に戻しておいてください。以降の解説は、元の状態を前提としたものになります。

6 ページの追加

ここでは、アプリに以下のような「About (自己紹介)」のページを追加し、サイト内外へのハイパーリンクを作成する方法、画像を表示 する方法などについて学びます。

img

6.1 アバター画像の準備と配置

プロジェクトフォルダの 直下public という名前のフォルダを作成してください。

Next.js において、プロジェクトフォルダの直下に作成した public という名前のフォルダは特別な意味を持ち、静的アセット (= 画像、PDF、Zipファイル、ファビコン、静的なHTML/CSS、robots.txt など) を配置するために使用します。

例えば、publictest.png という画像を配置しておけば http://localhost:3000/test.png のようにリクエストすることで直接的に画像が取得できるようになります。

ここでは、public のなかに images というサブフォルダを作成して「About」のページで表示するアバター画像 avatar.png を配置してください。画像は アスペクト比が「1:1」 のものを用意してください。あるいはこちらの画像を利用してください (AIで生成した画像なので自由に利用可能です)。

img

(プロンプト例)

次のキーワードに基づき、JRPGスタイルのドット絵でアバター画像を作成してください。
【キャラクタ】
・(キーワードを列挙)
【背景】
・(キーワードを列挙)
【雰囲気】
・(キーワードを列挙)

6.1.1 定着確認

6.2 Aboutコンポーネントの作成

/about というURL (パス) でアクセス可能なコンテンツを作成するために、src/app フォルダのなかにabout というサブフォルダを作成して page.tsx というファイルを新規作成してください。

img

page.tsx を次のような内容で保存してください。そして、開発モードでアプリを起動して http://localhost:3000/about のURLで「About」のコンテンツにアクセスできることを確認してください。

"use client";
import Image from "next/image";
import { twMerge } from "tailwind-merge";

const Page: React.FC = () => {
  return (
    <main>
      <div className="mb-5 text-2xl font-bold">About</div>

      <div
        className={twMerge(
          "mx-auto mb-5 w-full md:w-2/3",
          "flex justify-center"
        )}
      >
        <Image
          src="/images/avatar.png"
          alt="Example Image"
          width={350}
          height={350}
          priority
          className="rounded-full border-4 border-slate-500 p-1.5"
        />
      </div>
    </main>
  );
};

export default Page;

まず、上記の 第08行目 では「About」という文字列を出力しています。

また 第16行目 から 第23行目 にかけて Image というコンポーネントを使って「アバター画像」を出力しています (Imageコンポーネントは「第02行目」でインポートしています)。ここでは Image の src"/images/avatar.png"というパスを与えていることに注目してください。このパスは、プロジェクトフォルダの /public/images/avatar.png に対応しています。

Image要素 をラップしている div要素 では、レスポンシブデザインになるようにCSSを設定 しています。実際にブラウザの画面幅を変化させ アバター画像が適切に拡大縮小・レイアウトされること を確認してください。CSSクラスの詳細 (特に w-full md:w-2/3flex justify-center など) については、生成AIを利用して解決してください。このあたりを理解しておくと、標準的なレイアウトはそこそこ組むことができます。

なお、このファイルでは 第05行目 のように Page という名前で関数コンポーネントを作成していますが、AboutPageAbout のような任意の名前にすることができます (ただし パスカルケース としてください)。

6.2.1 演習 (10分)

Image コンポーネントの widthheight を属性を次のように変更した場合、レスポンシブ対応を含めてレンダリング出力がどのように変化するか確認してください。確認の際には、デベロッパーツールのコンソールも同時に確認してください (エラー出力の有無など)。

6.3 ヘッダにAboutへのリンクを設定

ヘッダ右端に配置した「About」の文字列に /about に対する ハイパーリンク を設定していきます。つまり、ヘッダの「About」をクリックしたときに /about に画面遷移するように設定していきます。

src/app/_components/Header.tsx を開いて、まずは import Link from "next/link"; を追加し、その Linkコンポーネント を使って以下のようにJSXを編集してください。

<div>
  <Link href="/">
    <FontAwesomeIcon icon={faFish} className="mr-1" />
    Header
  </Link>
</div>
<div>
  <Link href="/about">About</Link>
</div>

上記では「About」という文字列に /about のハイパーリンクを設定すると共に、「Header」に / へのハイパーリンクも設定しています。通常のHTMLでは <a href="...">...</a> を使ってハイパーリンクを設定しますが、Next.js では、遷移先がアプリ内となるハイパーリンクには next/link からインポートする Linkコンポーネント を使用します。

両差の違いについては、生成AIを利用して解決してください。

(プロンプト例)

Next.jsにおいて、「“next/link” からインポートしたLinkコンポーネントを使ってハイパーリンクを作成した場合」と「通常のaタグを使ってハイパーリンクを作成した場合」の違いについて教えてください。また、どのように使い分ければよいですか。

開発モードでアプリを起動し、意図したようにハイパーリンクが機能することを確認してください。

6.4 Next.jsにおける画像の表示

Next.js では、通常のHTMLの <img> タグではなく、next/image からインポートした Imageという組み込みのコンポーネント を使って画像を表示することが「推奨」されています。

<Image
  src="/images/avatar.png"
  alt="Example Image"
  width={350}
  height={350}
  priority
  className="rounded-full border-4 border-slate-500 p-1.5"
/>

Next.js における「画像の取り扱い」は、大きく「ローカル画像」と「リモート画像」の2種類に分類されます。

ここでの「ローカル画像」とは、プロジェクトフォルダ内に配置した画像のことで、これはさらに publicフォルダに配置した静的画像srcフォルダに配置した画像 に分けることができます。

publicフォルダの画像を Imageコンポーネントで表示するときは (面倒ですが) widthheight指定が必須となります。一方で、srcフォルダに配置した画像は ビルド時に最適化 され、サイズ圧縮、フォーマット変換、複数解像度への対応など、パフォーマンスとレスポンシブ性を考慮した処理 が自動的に適用されます (Imageコンポーネントでは widthheight の省略が可能です)。

publicフォルダの配置した画像へのアクセス

publicフォルダに配置した「画像」などのコンテンツは、URLを使ってダイレクトにアクセスができます。例えば、/images/avatar.png のように配置した画像は https://localhost:3000/images/avatar.png で取得することができます。

一方で、srcフォルダに配置した画像については、上記のようにURLを使ってダイレクトにアクセスすることはできません。

また「リモート画像」とは「クラウドストレージ」や「外部のウェブサービス」など、アプリの実行時にプロジェクト外から取得する画像 を指します。リモート画像を使用する場合は、画像を提供しているサービスの「ドメイン」を next.config.mjs に登録しておく必要があります。

Imageコンポーネントに関連して詳しいことが知りたい学生は公式ドキュメントを確認するか、生成AIを利用して解決してください。

(プロンプト例)

Next.js において、next/image からインポートした「Imageコンポーネント」を使用する際の注意点について教えてください。特に初心者がよくハマるポイントなどを教えてください。

Next.js において、<img> タグではなく、next/image からインポートした「Imageコンポーネント」を使って画像を表示するメリットを教えてください。

Next.js において、src フォルダに配置した画像については、ビルド時に最適化されると聞きました。具体的には、何のために、どのような処理が行われてるのか教えてください。

6.4.1 演習 (宿題: 30分)

Aboutページについて、「コンテンツ」と「デザイン」を各自で自由に設計して実装してください。

実装例(レスポンシブデザイン対応)

6.4.2 定着確認

7 投稿記事の一覧

アプリのトップページ (/) にアクセスしたとき、次のように「投稿記事の一覧」が表示されるような機能を実装していきます。記事の「タイトル」と「改行タグが反映された本文」に加えて、「投稿日」と「記事のカテゴリ (複数)」 についても表示されるように実装していきます。

img

7.1 実装の方針

ブログの投稿記事は、(最終的には) ウェブAPIを叩いて「JSON形式」でバックエンドから取得するように実装します。具体的には、次のような JSONリテラル (文字列化されたJSONデータ) をバックエンド経由でデータベースから取得することを想定します。DBは、リレーショナルデータベースを想定します。

[
  {
    "id": "24f932b8-231b-429b-b9dc-569f07ba16a7",
    "createdAt": "2024-10-24T22:37:17.367Z",
    "title": "投稿2",
    "content": "夏は夜。<br/>月のころは...(略)",
    "coverImage": {
      "url": "https://w1980.blob.core.windows.net/pg3/cover-img-green.jpg",
      "height": 768,
      "width": 1365
    },
    "categories": [
      {
        "id": "587ac4ab-92de-450c-9423-5e091d16ecb5",
        "name": "TypeScript"
      }
    ]
  },
  {
    "id": "36b7c693-4cce-4d73-afa3-acb54a404290",
    "createdAt": "2024-10-22T11:22:34.684Z",
    "title": "投稿1",
    "content": "春はあけぼの。<br/>やうやう白くなりゆく...(略)",
    "coverImage": {
      "url": "https://w1980.blob.core.windows.net/pg3/cover-img-purple.jpg",
      "height": 768,
      "width": 1365
    },
    "categories": [
      {
        "id": "587ac4ab-92de-450c-9423-5e091d16ecb5",
        "name": "TypeScript"
      },
      {
        "id": "5cf22131-bac8-4bd0-be8e-757cec2bcc9a",
        "name": "React"
      }
    ]
  }
]

ただし、ウェブAPIからの取得の前に、まずはローカルに配置した _mock/dummyPosts.ts から上記の取得することからはじめていきます。

ここで、以下の2点に注意してください。

  1. "createdAt" は記事が作成され日時を「ISO8601形式」で表したものですが、末尾に Z が付いている点です。この末尾の Z は、その日時が UTC(協定世界時)であること を示しています。そのため、「投稿1」の "2024-10-22T11:22:34.684Z" は、日本時間 (JST) でいえば2024-10-25 07:37:17.367 を意味します。

  2. "content": には HTMLタグを含んだ文字列 が格納されていま1す。ブログ記事中の「改行」を画面に反映させるためには HTMLタグを有効化する必要がありますが、許可するタグを制限し適切なサニタイズ処理を実装しないと XSS(クロスサイトスクリプティング)攻撃のリスク が生じます。

7.2 投稿記事のモックを生成

ソフトウェア開発において、実際のデータやシステムの振る舞いを模倣した仮のオブジェクトやデータ のことを モック (Mock) といいます。このブログアプリにおいても、最終的にはウェブAPIを使用してバックエンドから投稿記事のデータを取得しますが、ここではまずは「モック」を作成し、それを利用してデータを取得します。

モックを作成するにあたり、TypeScript環境では先に「」を定義する必要があります。src/app フォルダのなかに _types フォルダを作成して、各型を定義した「Category.ts」「CoverImage.ts」「Post.ts」という3つのファイルを作成してしてください。

なお、型ファイルの名前は、慣例に従って パスカルケース で命名してください。また、これらは (JSXを含まない) 純粋なTypeScriptファイルなので 拡張子は「.tsx」ではなく「.ts としてください (React関数コンポーネントのように JSX構文を使うファイル だけを拡張子 .tsx としてください)。

export type Category = {
  id: string;
  name: string;
};
export type CoverImage = {
  url: string;
  width: number;
  height: number;
};
import { Category } from "./Category";
import { CoverImage } from "./CoverImage";

export type Post = {
  id: string;
  title: string;
  content: string;
  createdAt: string;
  categories: Category[];
  coverImage: CoverImage;
};

次に src/app フォルダのなかに _mocks フォルダを作成し、そこに dummyPosts.ts というファイルを作成してください。そして、投稿記事の一覧を格納した dummyPosts というオブジェクトを作成してください。

最終的に、次のようなフォルダ構成になっていることを確認してください。

img

7.3 記事一覧の表示

アプリのトップページ (/) にアクセスしたとき、次のように「投稿記事の一覧」が表示されるようにコンポーネントを追加・実装していきます。

img

まずは、src/app/_components のなかに、次のような PostSummary.tsx を作成してください。PostSummaryコンポーネントは 投稿記事の「概要」を表示するためのコンポーネント になります。

"use client";
import type { Post } from "@/app/_types/Post";

type Props = {
  post: Post;
};

const PostSummary: React.FC<Props> = (props) => {
  const { post } = props;
  return (
    <div className="border border-slate-400 p-3">
      <div className="font-bold">{post.title}</div>
      <div>{post.content}</div>
    </div>
  );
};

export default PostSummary;

この PostSummaryコンポーネント は Props (第04行目 から 第06行目) として、(単一の) 投稿記事 を受け取り、その titlecontent を表示するコンポーネントとなっています。

次に、上記で作成した PostSummaryコンポーネント を呼び出すように src/app/page.tsx (= / にアクセスしたとき (トップページにアクセスしたとき) に読み込まれるファイル) を以下のように編集してください。第04行目 で「PostSummaryコンポーネント」をインポートして、第41行目から第43行目mapメソッド のなかで、呼び出しています。この処理の理解が怪しいときはTodoアプリの解説に戻って復習してください。

"use client";
import { useState, useEffect } from "react";
import type { Post } from "@/app/_types/Post";
import PostSummary from "@/app/_components/PostSummary";
import dummyPosts from "@/app/_mocks/dummyPosts";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";

const Page: React.FC = () => {
  // 投稿データを「状態」として管理 (初期値はnull)
  const [posts, setPosts] = useState<Post[] | null>(null);

  // コンポーネントが読み込まれたときに「1回だけ」実行する処理
  useEffect(() => {
    // 本来はウェブAPIを叩いてデータを取得するが、まずはモックデータを使用
    // (ネットからのデータ取得をシミュレートして1秒後にデータをセットする)
    const timer = setTimeout(() => {
      console.log("ウェブAPIからデータを取得しました (虚言)");
      setPosts(dummyPosts);
    }, 1000); // 1000ミリ秒 = 1秒

    // データ取得の途中でページ遷移したときにタイマーを解除する処理
    return () => clearTimeout(timer);
  }, []);

  // 投稿データが取得できるまでは「Loading...」を表示
  if (!posts) {
    return (
      <div className="text-gray-500">
        <FontAwesomeIcon icon={faSpinner} className="mr-1 animate-spin" />
        Loading...
      </div>
    );
  }

  // 投稿データが取得できたら「投稿記事の一覧」を出力
  return (
    <main>
      <div className="mb-2 text-2xl font-bold">Main</div>
      <div className="space-y-3">
        {posts.map((post) => (
          <PostSummary key={post.id} post={post} />
        ))}
      </div>
    </main>
  );
};

export default Page;

または、ここでは 第14行目 からの useEffect が処理において極めて重要な意味を持ちます。useEffect を使用することで、コンポーネントが読み込まれたときに 1回だけ ウェブAPIをコールして (現時点はモックから) データが取得される仕組み をつくっています。

もし、useEffect使用せずに処理を実装をすると、次のように 無限ループ が発生します。

  1. ウェブAPIをコールしてデータを取得
  2. 取得したデータを setPosts に与えて状態を更新 (state更新)
  3. 状態が更新されたので再レンダリング (=Page関数が再実行❗)
  4. ウェブAPIをコールしてデータを取得
  5. 取得したデータを setPosts に与えて状態を更新 (state更新)
  6. 状態が更新されたので再レンダリング (=Page関数が再実行❗)
  7. 😱😱無限ループ😱😱

このような無限ループは、API使用制限(Rate Limit)への抵触従量課金制のAPIを使用している場合は予期せぬ高額請求 (いわゆるAPI破産やクラウド破産)サーバへの過剰な負荷 につながるため、十分に注意してください。

なお、前回講義の でも紹介しましたが useEffect については「初心者向け!ReactのuseEffectが何してるのか20分で説明」の動画、ウェブ検索、生成AIを活用して理解を深めてください。

7.4 動作確認

ここまでの実装について、開発モードで実行確認する次のような出力が得られることが分かります。つまり、HTMLタグが サニタイズ (無害化) され出力されていることが確認できます。

img

Next.js (React) では、JSXで <div>{post.content}</div> のように記述すると、悪意あるスクリプトの実行を防ぐために コンテンツが自動的にサニタイズ (無害化・エスケープ処理) されます。

ただし、信頼できるコンテンツを HTML として意図的に描画したい場合は、次のように dangerouslySetInnerHTML 属性を使用します。

// <div>{post.content}</div>
<div dangerouslySetInnerHTML={{ __html: post.content }} />

実際に、src/app/_components/PostSummary.tsx を書き換えて、post.content<br/> が「改行」に置き換わって表示されることを確認してください。

なお、改行や太字/斜線/下線などの 特定のタグだけを許可 し、それ以外のタグ (例えば <script> などの危険なタグ) は削除したいときには dompurify (Next.jsでバックエンドでも使用する可能性があるのであれば isomorphic-dompurify) などのライブラリを使用します。詳しい使い方は生成AIなどで調べてください。

(プロンプト例)

TypeScript を採用した Next.js 環境において、isomorphic-dompurify を利用し、HTMLコンテンツから「改行」と「太字・斜線・下線」以外のタグを削除したいです。どのようにすればよいですか。

7.4.1 定着確認

7.4.2 演習 (宿題: 80分)

次のように「投稿日」と「カテゴリ」が表示されるように src/app/_components/PostSummary.tsx をアップデートしてください。「レイアウト」や「デザイン (装飾)」は各自で自由に設計してください。必要に応じて dummyPosts.ts にデータを追加してください。

img