メインコンテンツまでスキップ

Next.jsで猫画像ジェネレーターを作ろう

Next.jsの概要

Next.jsは、Webアプリケーションを作るためのフレームワークです。Next.jsはReactをベースに、モダンなWeb開発に必要な次の機能を追加しています。

  • ルーティング: 所定のディレクトリ構成とファイル名でページを自動的にルーティング
  • パフォーマンス最適化: サーバー側で事前にページを生成し、高速な初期表示を実現。画像の最適化やコード分割も自動で行う。
  • CSSフレームワーク: Tailwind CSSやCSS Modulesなどのスタイリング方法をサポート
  • バンドラー: webpackやBabelなどの設定を内部で行い、開発者が設定を気にする必要がない

また、UIなどのクライアントサイドだけでなく、サーバーサイドの処理もサポートしています。たとえば、データベースや外部APIとの通信をNext.jsから直接行えます。簡単なJSON APIを持たせることも可能です。

Next.jsはVercel社が開発を推進しており、同社はVercelというホスティングサービスを提供しています。そのため、Next.jsで構築したアプリケーションは簡単に公開できます。

このように、本格的なWebアプリケーションの開発にすぐさま臨めるようになっているのがNext.jsの魅力です。

これから作るもの

このチュートリアルでは、題して「猫画像ジェネレーター」です。どんなものかというと、ボタンを押したら、猫画像のAPIから画像のURLを取得し、ランダムに可愛い猫画像を表示するシンプルなウェブアプリケーションです。

最終的な成果物はデモサイトで確認できます。チュートリアルを開始する前に事前に触ってみることで、各ステップでどんな実装をしているかのイメージが掴みやすくなります。また、完成形のソースコードはGitHubでご覧いただけます。

このチュートリアルで学ぶこと

このチュートリアルでは、実務でよく使うNext.jsの機能を学べます。具体的には次のような内容です。

  • Next.jsの新規プロジェクトの作成
  • App Routerの使い方
  • サーバーコンポーネントとクライアントコンポーネント
  • サーバーアクションの使い方
  • 外部API連携
  • 機密情報となる資格情報(APIキー)の取り扱い

このチュートリアルに必要なもの

このチュートリアルで必要なものは次のとおりです。

  • Node.js v22以上
  • npm v10以上 (Node.jsに同梱)
  • ブラウザ (このチュートリアルではGoogle Chromeを想定しています)

Node.jsの導入については、開発環境の準備をご覧ください。

Next.jsをセットアップする

最初にnpx create-next-appコマンドでプロジェクトを作成します。random-catはプロジェクト名となる部分です。この部分は好きな名前でも構いませんが、本チュートリアルではrandom-catとして話を進めます。

sh
npx create-next-app random-cat
sh
npx create-next-app random-cat

このコマンドを実行すると、対話的な設定が始まります。初めてcreate-next-appを実行する場合は、create-next-appを導入していいか尋ねられるので、エンターキーを押して進めてください。

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

create-next-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 your code inside a `src/` directory? … No / Yes
Would you like to use App Router? (recommended) … No / Yes
Would you like to use Turbopack for `next dev`? … No / Yes
Would you like to customize the import alias (`@/*` by default)? … No / Yes

プロジェクトのセットアップが完了したら、作成されたディレクトリに移動してください。

sh
cd random-cat
sh
cd random-cat

プロジェクトのファイル構成が次のようになっているか確認してください。

text
.
├── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── node_modules/
├── public/
├── .gitignore
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── README.md
└── tsconfig.json
text
.
├── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── node_modules/
├── public/
├── .gitignore
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── README.md
└── tsconfig.json

開発サーバーを起動する

次のコマンドを実行して、開発サーバーを起動してください。

sh
npm run dev
sh
npm run dev

開発サーバーが起動したら、ターミナルに表示されているURLにブラウザでアクセスしてください。デフォルトではhttp://localhost:3000です。

ブラウザで表示されたNext.jsアプリの初期画面。中央にNext.jsロゴと「app/page.tsx を編集して開始」などのガイド、下部に「Deploy now」ボタンがある

ページコンポーネント

Next.jsでは、appディレクトリ配下の構造がページのルーティングに対応します。たとえば、app/page.tsx/にアクセスしたときに表示されるページとなります。app/about/page.tsxなら/aboutへのアクセスで表示されます。

このpage.tsxファイルのことを、Next.jsの用語でページコンポーネント(page component)と呼びます。

トップページのページコンポーネントを作る

app/page.tsxを次のように編集して、トップページのページコンポーネントを作成します。これは「猫画像予定地」が表示されるだけの単純なものです。

app/page.tsx
tsx
export default function Home() {
return <div>猫画像予定地</div>;
}
app/page.tsx
tsx
export default function Home() {
return <div>猫画像予定地</div>;
}

Next.jsにファイルをページコンポーネントとして認識させるには、次の2つのルールを守る必要があります:

  1. ファイル名はpage.tsxであること
  2. 関数がexport defaultでエクスポートされていること

これさえ守れば、appディレクトリ配下にファイルを作成するだけで、自動的にルーティングされます。関数名は何でも構いませんが、HomePageなど、ページコンポーネントとしてわかりやすい名前を使うことが一般的です。

コンポーネントを実装したら、ブラウザをリロードして画面に「猫画像予定地」と表示されているか確認してください。

ブラウザで表示されたNext.jsアプリ。左上に「猫画像予定地」というテキストのみがあるプレースホルダー画面

The Cat API

このチュートリアルでは猫の画像をランダムに表示するにあたりThe Cat APIを利用します。このAPIは猫の画像を取得したり、品種ごとの猫の情報を取得したりできます。

このAPIは無料で月間10,000リクエストまで利用できます。また、一度に要求する画像の数が10枚までであれば、APIキー認証なしに利用できます。このチュートリアルの実施にあたっては、いずれの条件も満たすはずなので、有料プランの契約やAPIキーの取得は必要ありません。

今回のチュートリアルではAPIドキュメントのQuickstartに記載されている/v1/images/searchへリクエストを投げてランダムな猫の画像を取得します。

試しにブラウザでhttps://api.thecatapi.com/v1/images/searchへアクセスしてみてください。ランダムな結果が返ってくるので値は少し違いますが、次のような構造のデータがレスポンスとして取得できます。レスポンスのデータ構造が配列になっている点に注意してください。

The Cat APIのレスポンスのサンプル
json
[
{
"id": "co9",
"url": "https://cdn2.thecatapi.com/images/co9.jpg",
"width": 900,
"height": 600
}
]
The Cat APIのレスポンスのサンプル
json
[
{
"id": "co9",
"url": "https://cdn2.thecatapi.com/images/co9.jpg",
"width": 900,
"height": 600
}
]

レスポンスにあるurlが猫画像のURLです。この値を取得して猫の画像をランダムに表示します。

画像を取得する関数を実装する

このステップでは、The Cat APIから猫画像を取得する関数を実装します。appディレクトリにfetch-image.tsというファイルを新たに作り、次のコードを書いてください。

app/fetch-image.ts
tsx
// APIから画像を取得する関数
export async function fetchImage() {
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}
app/fetch-image.ts
tsx
// APIから画像を取得する関数
export async function fetchImage() {
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}

fetchはHTTPリクエストでリソースを取得するブラウザ標準のAPIです。戻り値としてResponseオブジェクトを返します。Responseオブジェクトのjson()メソッドを実行することで、レスポンスのボディーをJSONとしてパースし、JavaScriptのオブジェクトとして取得できます。

fetchImage関数についているasyncキーワードは、この関数が非同期処理を行うことを示すものです。fetchres.jsonは非同期関数で、これらの処理を待つために、それぞれにawaitキーワードがついています。

この関数はexportキーワードを使って外部からインポートできるようにしています。後でこの関数をpage.tsxでインポートして使うためのものです。

ページにアクセスしたときにAPIを呼び出す

上で実装したfetchImage関数を使って、ページにアクセスしたときにAPIを呼び出すようにします。app/page.tsxを次のように編集してください。

app/page.tsx
tsx
import { connection } from "next/server"; // 追加
import { fetchImage } from "./fetch-image"; // 追加
 
export default async function Home() {
// ^^^^^(1) asyncキーワードを追加
// (2) ビルド時にfetchImageの結果が固定されないようにする
await connection();
// (3) APIから画像を取得
const image = await fetchImage();
// (4) 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image);
return <div>猫画像予定地</div>;
}
app/page.tsx
tsx
import { connection } from "next/server"; // 追加
import { fetchImage } from "./fetch-image"; // 追加
 
export default async function Home() {
// ^^^^^(1) asyncキーワードを追加
// (2) ビルド時にfetchImageの結果が固定されないようにする
await connection();
// (3) APIから画像を取得
const image = await fetchImage();
// (4) 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image);
return <div>猫画像予定地</div>;
}

このコードは、ページにアクセスがあったときに、The Cat APIを呼び出し、その結果をコンソールに表示するものです。

(2)のawait connection()は、fetchImage関数の呼び出しをリクエスト時に行わせるためのものです。Next.jsには、ビルド時にページを生成する静的サイト生成(SSG)という機能があります。await connection()がない状態でアプリをビルドすると、ビルド時にfetchImage関数が実行され、画像が固定化されます。その結果、実行時にブラウザをリロードしても画像が変わらない状態になります。

本アプリの要件としては、ブラウザをリロードしたときに、異なる画像を表示したいのでconnectionを呼び出しています。ちなみに、今はnpm start devで開発モードになっているため、connectionがなくてもリロードで画像が変わります。

(3)はfetchImage関数を呼び出す部分です。この関数は非同期関数なので、awaitキーワードを使って呼び出しています。JavaScriptにはawaitキーワードを使うには、関数にasyncキーワードをつける必要があります。これを忘れるとエラーが発生します。(1)でasyncキーワードを追加しているのはそのためです。

(4)は取得したデータをコンソールに表示する部分です。これは実装中に「正しくデータが取得できているか」を確認するための一時的なコードです。後で画像を表示する処理に置き換えます。

ここで、ブラウザの開発者ツールを開いてコンソールを確認してみましょう。ブラウザで右クリックして「検証」または「開発者ツール」を選択し、「Console」タブを選びます。

コンソールには「Home: 画像情報を取得しました」と表示されているはずです。これがconsole.logで出力したメッセージです。

Next.jsアプリの画面。上部に「猫画像予定地」と表示され、下半分でChrome DevToolsのコンソールが開き、猫画像のURLやサイズを含むログが出力されている

ログには「Server」と表示されています。これはHomeがサーバーサイドで実行されたためです。このことについては後ほど詳しく説明するので、一旦は気にしないでください。

関数の戻り値に型をつける

imageの型はany型になっています。any型は「型チェックを行わない」型です。そのため、存在しないプロパティを参照しても気づけずにバグが発生する危険性があります。

📄️ any型

TypeScriptのany型は、どんな型でも代入を許す型です。プリミティブ型であれオブジェクトであれ何を代入してもエラーになりません。

app/page.tsx
tsx
import { fetchImage } from "./fetch-image";
 
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
const image: any
// 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image.name); // 存在しないnameプロパティを参照している
return <div>猫画像予定地</div>;
}
app/page.tsx
tsx
import { fetchImage } from "./fetch-image";
 
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
const image: any
// 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image.name); // 存在しないnameプロパティを参照している
return <div>猫画像予定地</div>;
}

imageにはnameプロパティがありませんが、imageany型なので、上のような誤ったコードを書いてもTypeScriptは何も警告してくれません。

APIレスポンスの取り扱いはフロントエンドでバグが混在しやすい箇所なので、型を指定することで安全にAPIレスポンスを扱えるようにしていきます。

レスポンスに含まれる画像情報の型をImageとして定義します。そして、fetchImage関数の戻り値をPromise<Image>として型注釈します。

app/fetch-image.ts
tsx
// 画像情報の型定義
type Image = {
url: string;
};
 
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
// ^^^^^^^^^^^^^^^^型注釈を追加
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}
app/fetch-image.ts
tsx
// 画像情報の型定義
type Image = {
url: string;
};
 
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
// ^^^^^^^^^^^^^^^^型注釈を追加
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}

APIレスポンスにはurl以外のプロパティも含まれていますが、このアプリケーションで必要な情報はurlだけなので、他のプロパティの型の定義は省略しています。もし、他のプロパティも必要になった場合でも、Imageにプロパティの定義を追加していけばよいです。

fetchImage関数の戻り値が正しく型注釈がされていると、万が一APIレスポンスに存在しないプロパティを参照するコードを書いてしまっても、TypeScriptが警告するため問題に気がつけるようになります。

app/page.tsx
tsx
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
const image: Image
// 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image.name); // 存在しないnameプロパティを参照している
Property 'name' does not exist on type 'Image'.2339Property 'name' does not exist on type 'Image'.
return <div>猫画像予定地</div>;
}
app/page.tsx
tsx
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
const image: Image
// 画像URLをコンソールに表示
console.log("Home: 画像情報を取得しました", image.name); // 存在しないnameプロパティを参照している
Property 'name' does not exist on type 'Image'.2339Property 'name' does not exist on type 'Image'.
return <div>猫画像予定地</div>;
}
厳密なレスポンスのチェック

上のコードは、APIが返すデータ構造を100%信頼するコードになっています。JSON文字列をパースした結果が、次のような構造になっていることを暗黙的な前提としています:

  • 配列である
  • 配列の要素がオブジェクトである
  • そのオブジェクトにurlプロパティが存在する
  • urlプロパティの値が文字列である

場合によっては、APIが信頼できない場合もあるでしょう。より安全にするなら、APIレスポンスのチェック処理を追加することもTypeScriptでは可能です。fetchImage関数にチェック処理を追加した場合、次のようになります:

ts
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
// ^^^^^^^^^^^^^^^^型注釈
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images: unknown = await res.json();
// ^^^^^^^any型にさせないためにunknown型にする
console.log("画像情報を取得しました", images);
if (!isImageArray(images)) {
throw new Error("取得したデータが正しくありません");
}
if (!images[0]) {
throw new Error("取得したデータが空です");
}
return images[0]; // 画像情報の配列から最初の要素を返す
}
 
// Image型の配列であるかチェックする関数
function isImageArray(value: unknown): value is Image[] {
// valueが配列であること
if (!Array.isArray(value)) {
return false;
}
// 配列の要素が全てImage型であること
if (!value.every(isImage)) {
return false;
}
return true;
}
 
// Image型であるかチェックする関数
function isImage(value: unknown): value is Image {
// valueがオブジェクトであること
if (typeof value !== "object" || value === null) {
return false;
}
// valueにurlフィールドがあること
if (!("url" in value)) {
return false;
}
// urlフィールドが文字列であること
if (typeof (value as Image).url !== "string") {
return false;
}
return true;
}
ts
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
// ^^^^^^^^^^^^^^^^型注釈
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images: unknown = await res.json();
// ^^^^^^^any型にさせないためにunknown型にする
console.log("画像情報を取得しました", images);
if (!isImageArray(images)) {
throw new Error("取得したデータが正しくありません");
}
if (!images[0]) {
throw new Error("取得したデータが空です");
}
return images[0]; // 画像情報の配列から最初の要素を返す
}
 
// Image型の配列であるかチェックする関数
function isImageArray(value: unknown): value is Image[] {
// valueが配列であること
if (!Array.isArray(value)) {
return false;
}
// 配列の要素が全てImage型であること
if (!value.every(isImage)) {
return false;
}
return true;
}
 
// Image型であるかチェックする関数
function isImage(value: unknown): value is Image {
// valueがオブジェクトであること
if (typeof value !== "object" || value === null) {
return false;
}
// valueにurlフィールドがあること
if (!("url" in value)) {
return false;
}
// urlフィールドが文字列であること
if (typeof (value as Image).url !== "string") {
return false;
}
return true;
}

このチェック処理では、型が不明な値を安全に型付けするunknown型や、値の型をチェックしながら型付する型ガード関数などのTypeScriptのテクニックも用いています。これらについては、ここでは理解する必要はありませんが、興味のある方はチュートリアルを終えてから解説をご覧ください。

上のサンプルコードはTypeScriptの機能だけで安全性を高める書き方です。見てのとおり手続き的なコードで、「型安全性を高めるために、ここまで沢山のコードを書く必要があるのか」と感じることでしょう。この問題を解決するために、zodvalibottypeboxをはじめとした宣言的な型チェックライブラリを使うこともできます。興味があれば見てみてください。

チェック処理をどこまで厳密にやるかは自明な基準がありません。チェックすれば安全性は高まる一方で、実装保守コストは増加し、実行時パフォーマンスにも影響があります。バランスを取ることが実務では重要です。そして、TypeScriptはどのあたりにバランスを置く場合でも、柔軟に対応できる言語でもあります。

ページを表示したときに画像を表示する

画像データが取得できるようになったので、ここではページを表示したときに、猫の画像を表示する処理を書いていきましょう。

まず、画像を表示するためのReactコンポーネントを作成します。app/cat-image.tsxというファイルを新たに作成し、次のコードを記述してください。

app/cat-image.tsx
tsx
// コンポーネントの引数を定義する
type CatImageProps = {
url: string;
};
 
// 画像を表示するコンポーネント
export function CatImage({ url }: CatImageProps) {
return (
<div>
<img src={url} />
</div>
);
}
app/cat-image.tsx
tsx
// コンポーネントの引数を定義する
type CatImageProps = {
url: string;
};
 
// 画像を表示するコンポーネント
export function CatImage({ url }: CatImageProps) {
return (
<div>
<img src={url} />
</div>
);
}

このCatImageコンポーネントはurlというプロパティを受け取り、そのURLを使って猫の画像を表示する作りになっています。

次に、app/page.tsxを次のように編集して、猫画像を表示するコンポーネントを使うようにします。

app/page.tsx
tsx
import { CatImage } from "./cat-image"; // 追加
import { fetchImage } from "./fetch-image";
 
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
// 画像のURLを渡す
return <CatImage url={image.url} />;
}
app/page.tsx
tsx
import { CatImage } from "./cat-image"; // 追加
import { fetchImage } from "./fetch-image";
 
export default async function Home() {
// APIから画像を取得
const image = await fetchImage();
// 画像のURLを渡す
return <CatImage url={image.url} />;
}

CatImageコンポーネントをインポートして、Homeコンポーネントの中で使うようにします。CatImageコンポーネントにurlプロパティを渡すことで、猫の画像を表示するようになります。

page.tsxの変更が済んだら、猫の画像が表示されているか確認してみてください。画像がちゃんと表示されているでしょうか。

Next.jsアプリでページを表示した直後のブラウザ画面。実装したCatImageコンポーネントにより、テレビを見つめる2匹の猫の写真が全面に表示されている

ボタンクリックで画像が更新されるようにする

このセクションでは、ページ表示時に画像を読み込むだけでなく、ユーザーが「他のにゃんこも見る」ボタンをクリックしたときに新しい猫画像を取得して表示する機能を実装します。

app/cat-image.tsxを次のように編集してください。

app/cat-image.tsx
tsx
"use client"; // (1) use clientを指定
 
import { useState } from "react"; // 追加
import { fetchImage } from "./fetch-image";
 
type CatImageProps = {
url: string;
};
 
export function CatImage({ url }: CatImageProps) {
// (2) useStateを使って状態を管理
const [imageUrl, setImageUrl] = useState(url);
 
// (3) 画像を取得する関数を定義
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};
 
return (
<div>
{/* (4) ボタンの表示 */}
<button onClick={refreshImage}>他のにゃんこも見る</button>
{/* (5) 画像の表示 */}
{imageUrl && <img src={imageUrl} />}
</div>
);
}
app/cat-image.tsx
tsx
"use client"; // (1) use clientを指定
 
import { useState } from "react"; // 追加
import { fetchImage } from "./fetch-image";
 
type CatImageProps = {
url: string;
};
 
export function CatImage({ url }: CatImageProps) {
// (2) useStateを使って状態を管理
const [imageUrl, setImageUrl] = useState(url);
 
// (3) 画像を取得する関数を定義
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};
 
return (
<div>
{/* (4) ボタンの表示 */}
<button onClick={refreshImage}>他のにゃんこも見る</button>
{/* (5) 画像の表示 */}
{imageUrl && <img src={imageUrl} />}
</div>
);
}

変更内容をひとつひとつ見ていきましょう。

ts
// (2) useStateを使って状態を管理
const [imageUrl, setImageUrl] = useState<string>(url);
ts
// (2) useStateを使って状態を管理
const [imageUrl, setImageUrl] = useState<string>(url);

useStateはReactのフック(hook)のひとつで、コンポーネント内で状態を管理するための仕組みです。状態とはコンポーネントの表示に影響する値であり、ユーザーの操作や非同期処理によって変化する可能性のあるデータです。

const [imageUrl, setImageUrl] = useState(url);という記述を分解すると:

  • imageUrlは状態変数で、現在の猫画像のURLを保持します。
  • setImageUrlは状態を更新するための関数です。この関数を呼び出すことでimageUrlの値を変更できます。
  • useStateには初期値としてurlを渡しています。

imageUrlの状態が変わると、Reactはコンポーネントの再レンダリングを行います。つまり、CatImageコンポーネントが新しい状態を反映して画面に表示されるということです。このため、画像URLの取得時にsetImageUrlを呼び出すだけで、自動的に新たな画像が表示されるようになります。

useStateはクライアントサイドの機能なので、コンポーネントの先頭に"use client"というディレクティブを追加する必要があります。詳しくは後述します。

ts
// (3) 画像を取得する関数を定義
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};
ts
// (3) 画像を取得する関数を定義
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};

ここではrefreshImageという非同期関数を追加しています。この関数は画像を再取得する処理を行います。asyncキーワードをつけているのは、関数内でfetchImageawaitしているためです。refreshImageCatImage関数の中に書いている理由は、setImageUrl関数を使うためです。

関数の中も詳しく見てみましょう。まず、setImageUrl("")で画像URLを初期化しています。これはユーザー体験向上のためです。初期化しないと、再取得完了までに古い画像が表示され続けます。これだと、ボタンをクリックしても見た目の変化がありません。ユーザーが「本当にクリックが効いたのか?」と疑問に思う可能性があります。初期化にすることで、「現在新しい画像を読み込み中です」という状態を視覚的に伝えられます。特にレスポンスがときは、このステップが重要になります。

setImageUrl(image.url)を呼び出すと、imageUrl状態変数が更新され、コンポーネントが再レンダリングされます。新しいimageUrlの値を使って、JSXの{imageUrl && <img src={imageUrl} />}部分が再評価され、新しい猫画像が画面に表示されます。

つまり、このrefreshImage関数呼び出すだけで「画面上の猫画像を新しいものに差し替える」という視覚的な変化を起こせるようになるのです。

ts
{/* (4) ボタンの表示 */}
<button onClick={refreshImage}>他のにゃんこも見る</button>
ts
{/* (4) ボタンの表示 */}
<button onClick={refreshImage}>他のにゃんこも見る</button>

JSXのonClick={refreshImage}属性を使って、ボタンのクリックイベントとrefreshImage関数を紐づけています。この記述により、ユーザーがボタンをクリックしたときにrefreshImage関数が呼び出されるようになります。

ts
{/* (5) 画像の表示 */}
{imageUrl && <img src={imageUrl} />}
ts
{/* (5) 画像の表示 */}
{imageUrl && <img src={imageUrl} />}

このコードは、「条件付きレンダリング」という技法を使って画像の表示と非表示を切り替えています。これは論理演算子&&を利用したJSXの構文で、次のように動作します:

  1. imageUrlが空文字列の場合、左辺が「偽」扱いとなり、右辺の<img>は評価されません。よって、何も表示されません。
  2. imageUrlが空文字列でない場合、左辺が「真」扱いとなり、右辺の<img>が評価されます。よって、画像が表示されます。

これにより、imageUrlが空文字列の間では画像は表示されず、APIから画像URLが取得できてsetImageUrlで状態が更新されると画像が表示されるようになります。

JSXには文が書けない

上の条件分岐を見て「なぜ素直にif文を使わないのか?」と疑問の思ったかもしれません。これには理由があります。JSXの{}で囲った部分には、JavaScriptの式だけが書けます。ifは文であるため使うことができません。もし使おうとすると次の例のようにコンパイルエラーになります。

JSXの式には文が使えない
tsx
<div>{if (imageUrl) { <img src={imageUrl} /> }}</div>
JSXの式には文が使えない
tsx
<div>{if (imageUrl) { <img src={imageUrl} /> }}</div>

したがって、JSXの式で条件分岐するには論理演算子や三項演算子を使う必要があります。

tsx
<div>
{imageUrl && <img src="..." />} ── 論理積演算子
{!imageUrl || <img src="..." />} ── 論理和演算子
{imageUrl ? <img src="..." /> : "読み込み中"} ── 三項演算子
</div>;
tsx
<div>
{imageUrl && <img src="..." />} ── 論理積演算子
{!imageUrl || <img src="..." />} ── 論理和演算子
{imageUrl ? <img src="..." /> : "読み込み中"} ── 三項演算子
</div>;

ちなみに、JavaScriptではif文の代わりに論理演算子を使うパターンのことを、短絡評価(short-circuit evaluation)と呼びます。

これでクリックしたら画像が更新されるようになります。うまく動いているかブラウザで確認してみてください。

Next.jsのサーバーサイド機能

ここでは説明を省略してきたNext.jsのサーバーサイド機能について説明します。特に、後回しにした次の疑問に答えたいと思います。

  • Homeコンポーネントがサーバーで実行されたらしいが、どういうことか?
  • なぜクライアントサイド機能には"use client"を指定する必要があるのか?

歴史を振り返ると、Reactはブラウザ上でのみ動作するクライアントサイドのライブラリとして誕生しました。当初はクライアントサイドでUIを構築するさまざまな課題を解決してくれることから、広く使われるようになりました。

しかし、クライアントサイドだけでは解決できない課題もありました。特に、SEO(検索エンジン最適化)や初期表示速度の問題です。これらの問題を解決するために、Reactはサーバーサイドレンダリング(SSR)や静的サイト生成(SSG)などの機能を持つようになりました。

Next.jsは、SSRやSSGを簡単に実装できるだけでなく、APIルート(route)を使ってサーバーサイドのデータにもアクセスしやすいフレームワークとして人気を集めました。ここまで来ると、Next.jsとReactは単なる「Web APIのクライアント」ではなくなり、サーバーサイドとシームレスに連携するのが当たり前になったのです。

最近のNext.jsは、サーバーサイド機能を強力にサポートしています。

  • サーバーコンポーネント(Reactコンポーネントをサーバー側でレンダリングする機能)
  • APIルート (簡単なサーバーサイドAPIを作成する機能)
  • サーバーアクション(フォームの送信などのユーザー操作に応じてサーバー側の処理を実行する機能)
  • ミドルウェア(リクエストとレスポンスの間に処理を挟む機能)
  • 静的サイト生成

これらの機能を使うことで、次のようなことが簡単にできるようになります:

  • 初期ロード時のパフォーマンス向上
  • SEO(検索エンジン最適化)の強化
  • セキュリティの向上(APIキーなどの秘密情報をクライアントに公開せずに使用できる)
  • 認証や認可の実装
  • サーバーサイドのデータベース直接アクセス

本チュートリアルでは、サーバーサイド機能の中でも特によく使う「サーバーコンポーネント」に焦点を当てて説明します。

「サーバーコンポーネント」があるということはそれに対して「クライアントコンポーネント」もあります。まずはこの2つの特徴を見ていきましょう。

クライアントコンポーネント

クライアントコンポーネントは、ブラウザで実行されるReactコンポーネントです。ファイルの先頭に"use client"ディレクティブを記述することで、そのファイル内のコンポーネントがクライアントコンポーネントであることを明示します。このチュートリアルで作成したCatImageはクライアントコンポーネントでした。

クライアントコンポーネントの特徴としては次のようなものがあります:

  1. useStateuseEffectなどにより、クリックや入力などの操作に対応できる。
  2. windowdocumentなどのブラウザ専用APIが使える。
  3. コンポーネント内でUIの状態を保持できる

本チュートリアルで作成したCatImageコンポーネントでは、useStateで画像URLを保持し、ボタンクリックで画像を更新する処理を実装しました。これらはクライアントコンポーネントの特徴を活かしたものです。

サーバーコンポーネント

サーバーコンポーネントは、サーバー上でレンダリングされるReactコンポーネントです。"use client"なしでコンポーネントを定義すると、サーバーコンポーネントになります。上で作成したHomeコンポーネントはサーバーコンポーネントです。

サーバーコンポーネントの特徴

サーバーコンポーネントには、クライアントコンポーネントにはないいくつかの特徴があります。

  1. サーバー上のリソースへアクセスできる
    データベースやファイルシステム、インターネット非公開の内部APIなどが直接利用できます。
  2. 秘密情報を安全にあつかえる
    クライアントコンポーネントでは、APIキーなどの秘密情報を含めると、ブラウザの開発者ツールなどで見られてしまう恐れがあります。サーバーコンポーネントではAPI呼び出しの結果のみがクライアントに送られるので、秘密情報を安全に使えます。
  3. SEOに有利
    クライアントコンポーネントは、コンテンツがHTMLに含まれない場合があるため、検索エンジンがページの内容を理解できないことがあります。サーバーコンポーネントは、サーバー上でコンテンツがレンダリングされるため、SEO対策にも有利です。
  4. API通信が効率化できる
    クライアントサイドでのデータ取得では、多数のユーザーが同時にアクセスすると、同じデータに対する重複したAPIリクエストがバックエンドサーバーに大きな負荷をかけることがあります。サーバーコンポーネントでは、サーバー側でデータを取得し、Next.jsのキャッシュ機能と組み合わせることで、データ取得を効率化できます。これにより、バックエンドの負荷が軽減され、ユーザー体験も向上します。
  5. 初期表示速度が向上する
    サーバーコンポーネントは、サーバー上でレンダリングされるため、初期HTMLがクライアントに送信されるまでの時間が短縮されます。これにより、ユーザーがページを表示するまでの待ち時間が短縮され、ユーザー体験が向上します。

サーバーアクションを使う

解説ばかりだと退屈なので、ここからはコーディングに戻りましょう。上で作成したfetchImage関数は、サーバーコンポーネントであるHomeと、クライアントコンポーネントであるCatImageの両方から呼び出されています。

ここで疑問が生まれないでしょうか?「fetchImageはサーバーサイドで実行されるのか、それともクライアントサイドで実行されるのか?」という疑問です。答えは「両方」です。fetchImageHomeから呼び出されるときはサーバーサイドで実行され、CatImageから呼び出されるときはクライアントサイドで実行されます。

これを常にサーバーサイドで実行されるようにしてみましょう。fetch-image.tsの先頭に"use server"というディレクティブを追加します。これにより、fetchImage関数は常にサーバーサイドで実行されるようになります。

app/fetch-image.ts
tsx
"use server"; // 追加
 
// 画像情報の型定義
type Image = {
url: string;
};
 
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}
app/fetch-image.ts
tsx
"use server"; // 追加
 
// 画像情報の型定義
type Image = {
url: string;
};
 
// APIから画像を取得する関数
export async function fetchImage(): Promise<Image> {
const res = await fetch("https://api.thecatapi.com/v1/images/search");
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0]; // 画像情報の配列から最初の要素を返す
}

このように"use server"を指定された関数は、サーバーサイドで実行されるようになります。このような関数を「サーバーアクション」と呼びます。サーバーアクションは、クライアントコンポーネントからもまるでクライアントサイドの関数であるかのようにシームレスに呼び出すことができます。

ブラウザの開発ツールでネットワークを確認してみてください。「他のにゃんこも見る」ボタンをクリックしたときに発生する通信がlocalhostに対するものになっているのがわかります。

また、npm run devを実行しているターミナルには、サーバーサイドのログとして「fetchImage: 画像情報を取得しました」と表示されているはずです。これにより、fetchImage関数がサーバーサイドで実行されていることが確認できます。

APIキーを使う

The Cat APIは、APIキーを使わずに利用できるAPIです。しかし、実務で作るアプリケーションで利用するAPIでは、APIキーが必要になることが多いです。ここでは、APIキーをNext.jsでどう使ったらいいかを学び、実務で活かせるスキルを身につけてみましょう。

Next.jsでは、環境変数を使ってAPIキーを管理するのが一般的です。環境変数は.envファイルに定義します。例として、CAT_API_KEYという環境変数を使うことにします。プロジェクトのルートディレクトリに.envというファイルを作成し、次のように記述してください。

.env
bash
CAT_API_KEY=DEMO_KEY
.env
bash
CAT_API_KEY=DEMO_KEY

次に、環境変数をロードするためのコードを追加します。app/env.tsというファイルを作成し、次のように書いてください。

app/env.ts
ts
if (!process.env.CAT_API_KEY) {
throw new Error("環境変数 CAT_API_KEY が設定されていません");
}
 
export const CAT_API_KEY = process.env.CAT_API_KEY;
app/env.ts
ts
if (!process.env.CAT_API_KEY) {
throw new Error("環境変数 CAT_API_KEY が設定されていません");
}
 
export const CAT_API_KEY = process.env.CAT_API_KEY;

最後に、fetch-image.tsを次のように編集します。

app/fetch-image.ts
tsx
"use server";
 
import { CAT_API_KEY } from "./env"; // 追加
 
type Image = {
url: string;
};
 
export async function fetchImage(): Promise<Image> {
const res = await fetch("https://api.thecatapi.com/v1/images/search", {
headers: { "x-api-key": CAT_API_KEY }, // 追加
});
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0];
}
app/fetch-image.ts
tsx
"use server";
 
import { CAT_API_KEY } from "./env"; // 追加
 
type Image = {
url: string;
};
 
export async function fetchImage(): Promise<Image> {
const res = await fetch("https://api.thecatapi.com/v1/images/search", {
headers: { "x-api-key": CAT_API_KEY }, // 追加
});
const images = await res.json();
console.log("fetchImage: 画像情報を取得しました", images);
return images[0];
}

このようにAPIキーを環境変数として管理することで、ソースコードにAPIキーを直接書かずに済みます。

ビジュアルを作り込む

機能面が完成したので、最後にビジュアルデザインを作り込んでいきましょう。先ほど実装したサーバーコンポーネントとクライアントコンポーネントの構成を活かしながら、アプリケーションをより魅力的にします。

まず、スタイルシートを作成します。スタイルシートの内容は長くなるので、次のURLからスタイルシートをダウンロードしてください。ダウンロードしたら、appディレクトリにpage.module.cssとして保存してください。

https://raw.githubusercontent.com/yytypescript/random-cat/main/app/page.module.css

bash
cd app
curl https://raw.githubusercontent.com/yytypescript/random-cat/main/app/page.module.css > page.module.css
bash
cd app
curl https://raw.githubusercontent.com/yytypescript/random-cat/main/app/page.module.css > page.module.css

Next.jsでは、サーバーコンポーネントでもクライアントコンポーネントでもCSSモジュールを使用できます。.module.cssで終わるファイルはCSSモジュール(CSS Modules)と言うもので、CSSファイル内で定義したクラス名をTypeScriptからオブジェクトとして参照できるようになります。

ここではcat-image.tsxのクライアントコンポーネントにスタイルを適用します。まず、CSSモジュールを使うために次の2つの手順が必要です:

  1. インポート: import styles from "./page.module.css";という行を追加して、CSSモジュールをインポートします。これによりstylesオブジェクトを通してCSSのクラス名にアクセスできるようになります。
  2. className属性: JSX要素にclassName={styles.クラス名}という形式でスタイルを適用します。たとえば<div className={styles.page}>のように指定すると、CSSファイル内の.pageクラスのスタイルがそのdiv要素に適用されます。

この方法の利点は、クラス名の衝突を避けられることです。CSSモジュールは内部的にクラス名をユニークな値に変換するため、他のコンポーネントで同じクラス名を使っていても問題が起きません。

app/cat-image.tsx
tsx
"use client";
 
import { useState } from "react";
import { fetchImage } from "./fetch-image";
import styles from "./page.module.css"; // 追加
 
type CatImageProps = {
url: string;
};
 
export function CatImage({ url }: CatImageProps) {
const [imageUrl, setImageUrl] = useState<string>(url);
 
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};
 
return (
<div className={styles.page}>
<button onClick={refreshImage} className={styles.button}>
他のにゃんこも見る
</button>
<div className={styles.frame}>
{imageUrl && <img src={imageUrl} className={styles.img} />}
</div>
</div>
);
}
app/cat-image.tsx
tsx
"use client";
 
import { useState } from "react";
import { fetchImage } from "./fetch-image";
import styles from "./page.module.css"; // 追加
 
type CatImageProps = {
url: string;
};
 
export function CatImage({ url }: CatImageProps) {
const [imageUrl, setImageUrl] = useState<string>(url);
 
const refreshImage = async () => {
setImageUrl(""); // 初期化
const image = await fetchImage();
setImageUrl(image.url);
};
 
return (
<div className={styles.page}>
<button onClick={refreshImage} className={styles.button}>
他のにゃんこも見る
</button>
<div className={styles.frame}>
{imageUrl && <img src={imageUrl} className={styles.img} />}
</div>
</div>
);
}

以上でNext.jsを使った猫画像ジェネレーターの開発は完了です。

Next.jsアプリの最終デザインプレビュー。白背景のページ中央に黒枠で囲まれた茶トラ子猫の写真があり、その上に吹き出し風デザインのボタン「他のにゃんこも見る」が配置されている

プロダクションビルドと実行

Next.jsではnext buildを実行することで最適化されたプロダクション用のコードを生成でき、next startで生成されたプロダクションコードを実行できます。このチュートリアルではボイラテンプレートを利用しているので、package.jsonbuildコマンドとstartコマンドがすでに用意されています。npm run buildnpm run startを実行して本番用のアプリケーションを実行してみましょう。

sh
npm run build
npm run start
sh
npm run build
npm run start

アプリケーション起動後にhttp://localhost:3000へブラウザでアクセスをすることで、本番用のアプリケーションの実行を確認できます。