Next.js + TypeScript導入ガイド:モダンWebアプリ開発の第一歩
はじめに:なぜ今、Next.jsとTypeScriptなのか?
Webアプリケーション開発は日進月歩で進化しています。かつてはjQueryが主流だった時代から、React, Vue, AngularといったモダンなJavaScriptフレームワーク・ライブラリが登場し、SPA(シングルページアプリケーション)が中心となりました。しかし、SPAには初期ロード時間の長さやSEO対策の難しさといった課題も存在します。
そんな中で登場し、モダンWeb開発のデファクトスタンダードの一つとなりつつあるのが、ReactフレームワークであるNext.jsです。Next.jsは、Reactの強力なコンポーネントモデルと宣言的なUI構築のメリットを享受しつつ、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)といった機能を提供することで、SPAの課題を克服し、より高性能でSEOに強いアプリケーション開発を可能にします。さらに、ファイルシステムベースのルーティング、APIルート、コード分割など、開発者が煩雑な設定なしにこれらの恩恵を受けられるよう設計されています。
一方、TypeScriptは、JavaScriptに静的型付けを導入したスーパーセットです。JavaScriptの柔軟性は開発速度を上げる一方で、型関連の実行時エラーが発生しやすいという課題を抱えています。TypeScriptを使用することで、開発中に型エラーを検知できるようになり、バグの早期発見、コードの保守性向上、大規模プロジェクトでのチーム開発の効率化に大きく貢献します。エディタの補完機能やリファクタリング支援も強化され、開発体験が劇的に向上します。
Next.jsとTypeScriptは、それぞれが単体でも強力ですが、組み合わせることでその真価を発揮します。Next.jsで構築するアプリケーションは、しばしばある程度の規模になります。そのようなアプリケーションにおいて、TypeScriptによる型付けは、コードの構造を明確にし、予期せぬエラーを防ぎ、長期的なメンテナンスを容易にします。モダンなWeb開発の現場では、この組み合わせが標準的な技術スタックとなりつつあります。
本記事では、Next.jsとTypeScriptを初めて導入する方に向けて、プロジェクトのセットアップから基本的な機能、TypeScriptの活用方法までを詳細に解説します。約5000語にわたるこのガイドを通じて、Next.jsとTypeScriptを使ったモダンWebアプリケーション開発の第一歩を踏み出すための知識と自信を得られることを目指します。
開発環境の準備
Next.jsとTypeScriptでの開発を始めるには、まず以下の準備が必要です。
- Node.jsのインストール: Next.jsはNode.js上で動作します。公式サイト (https://nodejs.org/) から最新のLTS版をダウンロードしてインストールしてください。インストール後、ターミナルやコマンドプロンプトで
node -v
およびnpm -v
またはyarn -v
を実行し、バージョン情報が表示されることを確認してください。 - コードエディタ: TypeScriptの強力な型チェックや補完機能を最大限に活用するため、Visual Studio Code (VS Code) のような高機能なエディタを強く推奨します。VS CodeはTypeScriptのサポートが非常に優れています。
準備ができたら、Next.jsプロジェクトを作成しましょう。
Next.jsプロジェクトの作成(TypeScriptテンプレートを使用)
Next.jsプロジェクトを作成する最も簡単な方法は、公式が提供するcreate-next-app
ツールを使うことです。このツールは、プロジェクトの基本構造と必要な依存関係を自動的にセットアップしてくれます。TypeScriptを使用するには、プロジェクト作成時に--ts
(または--typescript
)オプションを付けます。
ターミナルを開き、プロジェクトを作成したいディレクトリに移動して、以下のコマンドを実行します。
“`bash
npx create-next-app@latest my-nextjs-ts-app –ts –use-npm
または yarn を使う場合:
yarn create next-app my-nextjs-ts-app –ts
“`
npx create-next-app@latest
:create-next-app
ツールの最新版を実行します。my-nextjs-ts-app
: 作成するプロジェクトのディレクトリ名です。任意の名前を指定できます。--ts
: TypeScriptテンプレートを使用することを指定します。これにより、TypeScriptの必要な設定ファイル(tsconfig.json
)や型定義ファイルが自動的に生成され、.js
ファイルの代わりに.tsx
ファイルが使われるようになります。--use-npm
: (任意) パッケージマネージャーとしてnpmを使用することを明示的に指定します。指定しない場合は、yarnまたはnpmのどちらかシステムにインストールされている方が使われます。
コマンドを実行すると、プロジェクトの作成が開始されます。いくつか設定に関する質問が表示される場合がありますが、特に理由がなければデフォルト設定(Enterキーを押す)で進めて問題ありません。
プロジェクトの作成が完了したら、作成されたディレクトリに移動します。
bash
cd my-nextjs-ts-app
そして、開発サーバーを起動します。
“`bash
npm run dev
または yarn を使う場合:
yarn dev
“`
開発サーバーが起動すると、通常は http://localhost:3000
でアプリケーションにアクセスできるようになります。ブラウザでこのURLを開き、Next.jsのデフォルトページが表示されることを確認してください。
これで、Next.jsとTypeScriptを組み合わせたプロジェクトの準備が完了しました。
プロジェクトの基本構造を理解する
create-next-app
で作成されたプロジェクトのディレクトリ構造を見てみましょう。
my-nextjs-ts-app/
├── .next/ # Next.jsが生成するビルド成果物やキャッシュ
├── node_modules/ # プロジェクトの依存関係
├── pages/ # ページコンポーネントを配置するディレクトリ(ファイルシステムベースのルーティング)
│ ├── _app.tsx # 全てのページ共通のレイアウトやプロバイダーを設定
│ ├── _document.tsx # ドキュメントの<body>や<head>をカスタマイズ (主にSSR用)
│ ├── api/ # APIルートを配置するディレクトリ
│ │ └── hello.ts # デフォルトで生成されるAPIルートの例
│ └── index.tsx # トップページ (http://localhost:3000/)
├── public/ # 静的ファイルを配置するディレクトリ (画像、フォントなど)
│ └── favicon.ico # ファビコンの例
├── styles/ # スタイルシートを配置するディレクトリ
│ ├── globals.css # グローバルスタイルシート
│ └── Home.module.css # CSS Modulesの例
├── .gitignore # Gitが無視するファイル・ディレクトリ指定
├── next-env.d.ts # Next.js固有の型定義ファイル (変更しない)
├── package.json # プロジェクトのメタデータと依存関係
├── README.md # プロジェクトの説明
├── tsconfig.json # TypeScriptの設定ファイル
└── next.config.js # Next.jsの設定ファイル (任意)
この中で特に重要なディレクトリとファイルは以下の通りです。
pages/
: このディレクトリ内のファイルは、自動的にルーティングされます。例えば、pages/about.tsx
を作成すると、/about
というURLでアクセスできるようになります。ファイル名がパスになります。index.tsx
:/
(ルートパス)に対応します。api/
: このディレクトリ内のファイルはAPIエンドポイントとして扱われます。例えば、pages/api/users.ts
は/api/users
というAPIエンドポイントになります。_app.tsx
: アプリケーション全体のエントリーポイントです。共通のヘッダーやフッター、CSSの読み込み、ReduxやContext APIなどの状態管理ライブラリの設定などをここで行います。_document.tsx
: サーバーサイドレンダリング時に、HTMLドキュメント(<html>
,<body>
など)をカスタマイズする場合に使用します。通常はデフォルトのままで問題ありませんが、フォントのプリロードやカスタムメタタグの追加など、より詳細な制御が必要な場合に使います。
public/
: このディレクトリ内のファイルは、そのまま/
から始まるパスで静的に提供されます。例えば、public/images/logo.png
というファイルを置くと、/images/logo.png
でアクセスできます。アプリケーションのルートに配置されるため、コード内からは/images/logo.png
のように参照します。styles/
: アプリケーションのスタイルシートを置く場所です。デフォルトではCSS Modulesを使った例が提供されています。tsconfig.json
: TypeScriptのコンパイラ設定ファイルです。プロジェクト全体のTypeScriptの挙動を制御します。next-env.d.ts
: Next.jsが提供する型定義ファイルです。Next.js固有のモジュール解決などをTypeScriptに認識させるために必要です。このファイルは変更しないでください。
これらの基本構造を理解することは、Next.jsプロジェクトで効率的に開発を進める上で不可欠です。
Next.jsのコアコンセプト:ファイルシステムベースのルーティング
Next.jsの最も便利な機能の一つは、ファイルシステムベースのルーティングです。pages
ディレクトリ内に.js
, .jsx
, .ts
, .tsx
ファイルを作成するだけで、そのファイルパスに基づいてURLが自動的に生成されます。
pages/index.tsx
->/
pages/about.tsx
->/about
pages/products/index.tsx
->/products
pages/products/detail.tsx
->/products/detail
動的ルーティング
特定のIDやスラッグに基づいてページを動的に表示したい場合は、ファイル名を角括弧 []
で囲みます。
pages/posts/[id].tsx
->/posts/1
,/posts/abc
など、/posts/
に続くパスセグメントをid
として受け取ります。pages/users/[userId]/profile.tsx
->/users/123/profile
など、/users/
に続くパスセグメントをuserId
として受け取ります。
動的なパスセグメントは、ページコンポーネント内で useRouter
フックを使って取得できます。
例: pages/posts/[id].tsx
“`typescript
import { useRouter } from ‘next/router’;
import React from ‘react’; // Reactをインポート
const PostDetail: React.FC = () => { // コンポーネントをReact.FCとして型付け
const router = useRouter();
const { id } = router.query; // クエリパラメータとしてidを取得
// id は string | string[] | undefined の可能性があります。
// 動的ルーティングの場合、通常は string | undefined ですが、
// /[…slug] のようなcatch-all routesの場合は string[] | undefined になります。
// ここでは単一の動的セグメントなので string | undefined と仮定します。
const postId = Array.isArray(id) ? id[0] : id;
if (!postId) {
// id がまだ利用可能でない場合や、エラーの場合の表示
return
;
}
// 取得した postId を使って記事の内容を表示するなどの処理
return (
Post ID: {postId}
{/ ここに記事の内容を表示 /}
);
};
export default PostDetail;
“`
useRouter().query
は、初期レンダリング時にはクエリパラメータがまだパースされていない場合があるため、undefined
になる可能性があります。また、複数のパラメータが同じ名前で渡された場合は文字列の配列になる可能性があります。そのため、型ガードなどを用いて適切に処理することが重要です。
リンクの作成
ページ間で遷移するには、Next.jsが提供する<Link>
コンポーネントを使用することを強く推奨します。<Link>
を使うことで、クライアントサイドルーティングが有効になり、ページ全体のリロードなしに高速な画面遷移が可能になります。また、必要に応じてページのプリフェッチ(事前読み込み)も行われます。
“`typescript
import Link from ‘next/link’;
import React from ‘react’;
const HomePage: React.FC = () => {
return (
Welcome to the Home Page!
);
};
export default HomePage;
“`
<Link>
コンポーネントの内部には、<a>
タグまたはカスタムコンポーネントを配置します。テキストリンクの場合は<a>
タグが一般的です。
href
属性には、遷移先のパスを指定します。動的パスに遷移する場合は、href="/posts/123"
のように実際のパスを指定するか、オブジェクト形式で pathname
と query
を指定する方法もあります。後者の形式を使うと、パス生成やクエリパラメータの取り扱いがより構造的になります。
レイアウトの適用
多くの場合、Webサイト全体で共通のヘッダー、フッター、ナビゲーションなどが必要です。Next.jsでは、pages/_app.tsx
ファイルを使ってアプリケーション全体に共通のレイアウトを適用できます。
_app.tsx
は、全てのページコンポーネントをラップするルートコンポーネントです。ここでは、Component
プロパティとして現在のページコンポーネント、pageProps
プロパティとしてそのページに渡されるPropsを受け取ります。
共通レイアウトを適用するには、_app.tsx
で Component
をカスタムレイアウトコンポーネントでラップします。
まず、共通レイアウトコンポーネントを作成します。例えば components/Layout.tsx
というファイルを作成します。
“`typescript
// components/Layout.tsx
import Head from ‘next/head’; // Headコンポーネントをインポート
import React, { ReactNode } from ‘react’; // ReactとReactNodeをインポート
// LayoutコンポーネントのPropsの型を定義
interface LayoutProps {
children: ReactNode; // 子要素はReactNode型
title?: string; // titleはオプションの文字列型
}
const Layout: React.FC
return (
{/* ヘッダーやナビゲーションなど共通要素 */}
<header style={{ background: '#eee', padding: '10px' }}>
<nav>
{/* ナビゲーションリンクなどを配置 */}
Nav Bar
</nav>
</header>
{/* 各ページの内容が表示される部分 */}
<main style={{ padding: '20px' }}>
{children} {/* childrenが各ページコンポーネントに対応します */}
</main>
{/* フッターなど共通要素 */}
<footer style={{ background: '#eee', padding: '10px', textAlign: 'center' }}>
© 2023 My Next.js App
</footer>
</div>
);
};
export default Layout;
“`
次に、pages/_app.tsx
を編集して、このLayout
コンポーネントで Component
をラップします。
“`typescript
// pages/_app.tsx
import type { AppProps } from ‘next/app’; // AppPropsの型をインポート
import Layout from ‘../components/Layout’; // 作成したLayoutコンポーネントをインポート
import ‘../styles/globals.css’; // グローバルCSSをインポート
// MyAppコンポーネントの型をAppPropsとして定義
function MyApp({ Component, pageProps }: AppProps) {
return (
// Layoutコンポーネントでページコンポーネントをラップ
);
}
export default MyApp;
“`
これで、アプリケーションの全てのページに共通のヘッダー、フッター、および基本的なスタイリングが適用されるようになります。Head
コンポーネントを使うことで、ページの<head>
タグの内容(タイトル、メタディスクリプションなど)をページごとにカスタマイズすることも可能です。
Next.jsのデータフェッチ:getServerSideProps, getStaticProps, getStaticPaths
モダンWebアプリケーションにおいて、データのフェッチは不可欠な要素です。APIからデータを取得したり、データベースから情報を読み込んだりすることが頻繁に発生します。Next.jsは、データのフェッチ方法についていくつかの強力な選択肢を提供しており、アプリケーションの要件(パフォーマンス、SEO、ビルド時間など)に応じて最適な方法を選択できます。
主要なデータフェッチメソッドは以下の3つです(これらはPages Routerにおけるサーバーサイドでのデータフェッチ方法です。クライアントサイドでのフェッチは通常のReactの方法で行えます)。
-
getServerSideProps
(SSR: Server-Side Rendering)- いつ使うか?: リクエストごとにサーバー側でデータをフェッチし、そのデータを使ってHTMLを生成したい場合。ユーザー認証情報に依存するページや、頻繁に更新される(かつ最新の情報が常に必要な)データを持つページに適しています。
- 仕組み: ページにアクセスがあるたびに、サーバー上でこの関数が実行されます。フェッチしたデータはページのPropsとして渡されます。
- メリット: 常に最新のデータを表示できる。SEOに有利(最初のHTMLにデータが含まれるため)。
- デメリット: リクエストごとにサーバー処理が発生するため、ページ表示に時間がかかる場合がある。サーバー負荷が高くなる可能性がある。
-
getStaticProps
(SSG: Static Site Generation)- いつ使うか?: ビルド時に一度だけデータをフェッチし、そのデータを使って静的なHTMLファイルを生成したい場合。ブログ記事、ドキュメント、製品一覧など、ビルド後に頻繁に更新されないデータを持つページに適しています。
- 仕組み:
next build
時にこの関数が実行されます。フェッチしたデータはページのPropsとして渡され、そのデータを含むHTMLファイルが生成されます。生成されたHTMLはCDNにキャッシュされ、高速に配信されます。 - メリット: 非常に高速なページ表示が可能(CDNから静的ファイルが配信されるため)。サーバー負荷が低い。SEOに有利。
- デメリット: データ更新頻度が高いページには不向き(データ更新を反映するには再ビルドが必要)。ビルド時間が増加する可能性がある(大量のページを生成する場合)。
-
getStaticPaths
(SSG for Dynamic Routes)- いつ使うか?:
[id].tsx
のような動的ルーティングを持つページでSSGを行いたい場合。ビルド時にどのパス(例:/posts/1
,/posts/2
など)の静的HTMLを生成するかを指定します。getStaticProps
と一緒に使われます。 - 仕組み:
next build
時にこの関数が実行され、SSG対象となる動的パスのリスト(例:{ params: { id: '1' } }
,{ params: { id: '2' } }
など)を返します。Next.jsは、返された各パスに対して、対応するページファイルのgetStaticProps
を実行し、静的HTMLを生成します。 - メリット: 動的なコンテンツを静的に生成できる。
- デメリット: ビルド時に生成するパスを事前に知っている必要がある。
- いつ使うか?:
これらの関数は、必ず pages
ディレクトリ内のページコンポーネントファイルから export
する必要があります。また、これらの関数はサーバーサイドでのみ実行されるため、ブラウザのAPI(window
, document
など)やクライアントサイドでしか実行できないコード(useEffect
内のコードなど)を含めることはできません。
getServerSideProps
の例とTypeScriptによる型付け
外部APIからIDに対応するユーザー情報を取得し、表示するページの例です (pages/users/[id].tsx
)。
まず、取得するユーザーデータの型を定義します。
typescript
// types/User.ts (共通の型定義ファイルとして管理するのがおすすめです)
export interface User {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
}
次に、pages/users/[id].tsx
ファイルを作成します。
“`typescript
// pages/users/[id].tsx
import { GetServerSideProps, InferGetServerSidePropsType } from ‘next’; // 型をインポート
import React from ‘react’; // Reactをインポート
import { User } from ‘../../types/User’; // 定義したUser型をインポート
// getServerSidePropsから返されるPropsの型を定義
// InferGetServerSidePropsTypeを使って、getServerSideProps関数の返り値から自動的に型を推論させます
type Props = InferGetServerSidePropsType
// ページコンポーネント
const UserDetail: React.FC
// user が null や undefined の場合のハンドリングも必要に応じて追加
if (!user) {
return
;
}
return (
User Detail
ID: {user.id}
Name: {user.name}
Username: {user.username}
Email: {user.email}
{/ その他のユーザー情報 /}
);
};
export default UserDetail;
// getServerSideProps関数
export const getServerSideProps: GetServerSideProps<{ user: User | null }> = async (context) => {
const { id } = context.params as { id: string }; // context.paramsの型を指定
const userId = parseInt(id, 10); // IDを数値に変換
// 外部APIからユーザー情報をフェッチ
const res = await fetch(https://jsonplaceholder.typicode.com/users/${userId}
);
const user: User | null = res.ok ? await res.json() : null; // エラー時も考慮して型を定義
// フェッチしたデータをpropsとして返す
return {
props: {
user, // ページコンポーネントに渡されるデータ
},
};
};
“`
getServerSideProps
関数では、context.params
から動的パスのセグメント(この場合は id
)を取得できます。context.params
の型は自動的には推論されないため、明示的に型アサーション(as { id: string }
)を行うか、より安全な方法として型ガードなどを使用することを検討してください。
GetServerSideProps
型にジェネリクスとして { user: User | null }
を指定することで、この関数が返す props
オブジェクトの型を明確に定義しています。
InferGetServerSidePropsType<typeof getServerSideProps>
を使うことで、getServerSideProps
の返り値の型から、ページコンポーネントが受け取るPropsの型を自動的に推論させることができます。これにより、型定義の重複を防ぎ、型安全性を高めることができます。
getStaticProps
と getStaticPaths
の例とTypeScriptによる型付け
ブログ記事一覧ページと、各記事の詳細ページをSSGで生成する例です。
まず、取得する記事(Post)データの型を定義します。
typescript
// types/Post.ts
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
次に、記事一覧ページ (pages/posts/index.tsx
) を作成します。
“`typescript
// pages/posts/index.tsx
import { GetStaticProps, InferGetStaticPropsType } from ‘next’;
import Link from ‘next/link’;
import React from ‘react’;
import { Post } from ‘../../types/Post’;
// getStaticPropsから返されるPropsの型を定義
type Props = InferGetStaticPropsType
// ページコンポーネント
const PostsList: React.FC
return (
Posts
-
{/ posts が配列であることを確認してから map する /}
-
/posts/${post.id}}>
{post.title}
{posts && posts.map((post) => (
))}
);
};
export default PostsList;
// getStaticProps関数 (ビルド時に実行される)
export const getStaticProps: GetStaticProps<{ posts: Post[] }> = async () => {
// 外部APIから記事一覧をフェッチ
const res = await fetch(‘https://jsonplaceholder.typicode.com/posts’);
const posts: Post[] = await res.json(); // APIのレスポンスがPost[]型であることを仮定
// フェッチしたデータをpropsとして返す
return {
props: {
posts,
},
// revalidate: 60 // 60秒ごとに再生成を試みる (ISR: Incremental Static Regeneration)
};
};
“`
getStaticProps
は、ビルド時に一度だけ実行され、全記事のリストを取得してPropsとしてページに渡します。これにより、/posts
ページはビルド時に静的なHTMLとして生成されます。revalidate
オプションを指定すると、Incremental Static Regeneration (ISR) が有効になり、指定した秒数(この例では60秒)ごとにバックグラウンドでページの再生成を試みるようになります。これにより、静的サイトのパフォーマンスを維持しつつ、データ更新にもある程度対応できるようになります。
次に、各記事の詳細ページ (pages/posts/[id].tsx
) を作成します。ここでは getStaticPaths
と getStaticProps
の両方を使用します。
“`typescript
// pages/posts/[id].tsx
import { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from ‘next’;
import React from ‘react’;
import { Post } from ‘../../types/Post’;
// getStaticPropsから返されるPropsの型を定義
type Props = InferGetStaticPropsType
// ページコンポーネント
const PostDetail: React.FC
// post が null や undefined の場合のハンドリング(fallback: true/blocking の場合)
if (!post) {
// fallback: true の場合、初回アクセス時はローディング表示などを行う
return
;
}
return (
{post.title}
{post.body}
);
};
export default PostDetail;
// getStaticPaths関数 (ビルド時に実行される)
export const getStaticPaths: GetStaticPaths = async () => {
// 全記事のIDを取得するなどして、生成したい動的パスのリストを作成
const res = await fetch(‘https://jsonplaceholder.typicode.com/posts’);
const posts: Post[] = await res.json();
// パスリストを生成: [{ params: { id: ‘1’ } }, { params: { id: ‘2’ } }, …]
const paths = posts.map((post) => ({
params: { id: post.id.toString() }, // paramsの値は文字列である必要がある
}));
return {
paths, // 生成するパスのリスト
fallback: false, // または true, ‘blocking’
// fallback: false: pathsに含まれないパスは404エラーになる
// fallback: true: pathsに含まれないパスは初回アクセス時にSSGを試み、以降は静的に配信。ローディング表示が必要。
// fallback: ‘blocking’: fallback: true と似ているが、初回アクセス時にサーバー側でSSGが完了するまで待機する。ローディング表示は不要。
};
};
// getStaticProps関数 (各パスに対してビルド時に実行される)
export const getStaticProps: GetStaticProps<{ post: Post | null }, { id: string }> = async (context) => {
const { id } = context.params!; // context.params は必ず存在する (fallback: falseの場合)
const postId = parseInt(id, 10);
// 特定のIDの記事をフェッチ
const res = await fetch(https://jsonplaceholder.typicode.com/posts/${postId}
);
const post: Post | null = res.ok ? await res.json() : null;
// post が見つからなかった場合なども考慮
if (!post) {
return {
notFound: true, // 404ページを表示
};
}
return {
props: {
post,
},
revalidate: 60, // ISRを有効にする場合
};
};
“`
getStaticPaths
では、paths
配列でビルド時に生成したい動的パスのリストを返します。各要素の params
オブジェクトのキーはファイル名([id].tsx
なら id
)と一致させる必要があり、値は文字列である必要があります。
fallback
オプションは、paths
リストに含まれないパスへのアクセスがあった場合の挙動を制御します。false
にすると、リストにないパスは全て404エラーになります。true
や 'blocking'
にすると、初回アクセス時に動的にSSGを試みることができます(ISRと組み合わせて使うことが多いです)。
getStaticProps
では、context.params
から現在のパスの動的セグメントを取得できます。この関数は、getStaticPaths
で返された各パスに対して実行されます。GetStaticProps
型の2つ目のジェネリクス引数に { id: string }
を指定することで、context.params
の型を明確にできます。
notFound: true
を返すと、そのパスに対して404ページが表示されます。これは、例えば存在しないIDの記事にアクセスされた場合などに便利です。
どちらのデータフェッチ方法を選ぶべきか?
特徴 | getServerSideProps (SSR) |
getStaticProps (SSG) + getStaticPaths (動的SSG) |
---|---|---|
データ鮮度 | 常に最新(リクエストごと) | ビルド時のデータ(ISRを使えば定期的に更新可能) |
パフォーマンス | 初回ロードに時間がかかることがある(サーバー処理待ち) | 非常に高速(CDNから静的ファイルを配信) |
サーバー負荷 | リクエストごとに負荷発生 | ビルド時に負荷集中、リクエスト時は低い |
SEO | 非常に有利 | 非常に有利 |
ユースケース | ユーザー固有のデータ、頻繁に更新されるデータ、最新必須のデータ | ブログ記事、ドキュメント、製品一覧、LP、FAQなど |
TypeScript | GetServerSideProps , InferGetServerSidePropsType |
GetStaticProps , GetStaticPaths , InferGetStaticPropsType |
一般的には、可能な限りSSGを使うのが推奨されます。なぜなら、SSGは最高のパフォーマンスと低いサーバー負荷を実現できるからです。データの鮮度がそれほど重要でないページ(ブログ記事など)や、更新頻度が低いページに最適です。動的ルーティングのページでも getStaticPaths
を使えばSSGが可能です。
最新データが必須で、ユーザー固有の情報などリクエストごとに内容が変わるページにはSSR (getServerSideProps
) を使います。
クライアントサイドでのデータフェッチ: 上記のメソッドはサーバーサイドでのフェッチですが、Next.jsでも通常のReactアプリケーションと同様に、コンポーネントのライフサイクル(useEffect
フックなど)でクライアントサイドからAPIを呼び出してデータをフェッチすることも可能です。これは、SEOが重要でないページや、初期表示後のインタラクションで追加データをロードする場合などに適しています。
Next.jsにおけるAPIルート
Next.jsのもう一つの便利な機能は、APIルートです。pages/api
ディレクトリ内にファイルを作成すると、サーバーレス関数として機能するAPIエンドポイントを簡単に構築できます。これは、フロントエンドアプリケーションからアクセスするバックエンドロジック(例:データベース操作、外部APIとの連携、フォーム送信処理など)をNext.jsプロジェクト内にまとめて記述したい場合に非常に便利です。
APIルートファイルは、デフォルトでHTTPメソッド(GET, POST, PUT, DELETEなど)を処理するリクエストハンドラ関数を export default
します。
例: /api/hello
エンドポイント (pages/api/hello.ts
)
create-next-app
でプロジェクトを作成した際に、既に pages/api/hello.ts
というファイルが生成されています。
“`typescript
// pages/api/hello.ts
// Request, Responseの型をNext.jsからインポート
import type { NextApiRequest, NextApiResponse } from ‘next’;
// レスポンスデータの型を定義
type Data = {
name: string;
};
// APIルートのリクエストハンドラ関数
// req: Incoming message object
// res: Server response object
export default function handler(
req: NextApiRequest, // リクエストオブジェクトの型を指定
res: NextApiResponse // レスポンスオブジェクトの型を指定
) {
// HTTPメソッドに応じて処理を分岐することも多いですが、この例ではGETリクエストのみを想定
res.status(200).json({ name: ‘John Doe’ }); // JSONレスポンスを返す
}
“`
このAPIルートは、http://localhost:3000/api/hello
にGETリクエストを送信すると、{ "name": "John Doe" }
というJSONレスポンスを返します。
NextApiRequest
と NextApiResponse
型を使うことで、リクエストオブジェクト (req
) やレスポンスオブジェクト (res
) のプロパティ(req.method
, req.body
, res.status
, res.json
など)に対する型補完や型チェックが有効になります。
APIルートでのPOSTリクエスト処理とTypeScript
フォームから送信されたデータを受け取るAPIルートの例です (pages/api/submit-form.ts
)。
“`typescript
// pages/api/submit-form.ts
import type { NextApiRequest, NextApiResponse } from ‘next’;
// リクエストボディの型を定義
interface FormData {
name: string;
email: string;
message: string;
}
// レスポンスデータの型を定義
type Result = {
message: string;
receivedData?: FormData; // 成功時には受け取ったデータも含む
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// POSTメソッド以外は許可しない
if (req.method !== ‘POST’) {
res.setHeader(‘Allow’, [‘POST’]); // 許可するメソッドをヘッダーに設定
return res.status(405).json({ error: Method ${req.method} Not Allowed
});
}
// リクエストボディを取得し、型アサーションまたは型ガードを行う
const formData = req.body as FormData; // ここではシンプルに型アサーションを使用
// データの検証(例:必須フィールドのチェック)
if (!formData.name || !formData.email || !formData.message) {
return res.status(400).json({ error: ‘Missing required fields.’ });
}
// ここでデータベースへの保存や外部APIへの送信などの処理を行う
console.log(‘Received form data:’, formData);
// 成功レスポンスを返す
res.status(200).json({ message: ‘Form submitted successfully!’, receivedData: formData });
}
“`
この例では、req.method
をチェックしてPOSTリクエストのみを受け付けるようにしています。req.body
から送信されたデータを取得できますが、デフォルトでは any
型になるため、適切な型を定義し、型アサーション(as FormData
)を行うか、より厳密にバリデーションと型ガードを行う必要があります。
APIルートを使うことで、シンプルなバックエンド機能をNext.jsプロジェクト内に手軽に実装できるため、小~中規模のアプリケーションやプロトタイピングにおいて非常に効果的です。
TypeScriptの活用:基本から実践まで
プロジェクトにTypeScriptを導入したことで、開発中に型安全性の恩恵を受けられるようになりました。ここでは、Next.js開発でよく使うTypeScriptのパターンと、型定義の重要性について説明します。
基本的な型
JavaScriptのプリミティブ型に加えて、TypeScriptでは以下の基本的な型が使えます。
string
: 文字列 ('hello'
)number
: 数値 (42
,3.14
)boolean
: 真偽値 (true
,false
)Array<T>
またはT[]
: 型T
の要素を持つ配列 (Array<number>
,string[]
)object
: オブジェクト型(厳密にはより具体的なオブジェクト型を使うのが良い)any
: あらゆる型(型の恩恵を放棄する場合に使用、乱用は避けるべき)unknown
:any
に似ているが、より安全(使用する前に型チェックが必要)void
: 関数が何も返さないことを示すnull
,undefined
: それぞれnull
,undefined
を示すunion types
(A | B
): 型A
または型B
intersection types
(A & B
): 型A
かつ型B
(両方のメンバーを持つ型)literal types
: 特定の値のみを許可する型 ('open'
,'closed'
,1
,true
)
インターフェース (Interface) と 型エイリアス (Type Alias)
複雑なオブジェクトや関数の型を定義する場合、インターフェースまたは型エイリアスを使用します。
“`typescript
// インターフェースでオブジェクトの型を定義
interface UserProfile {
id: number;
name: string;
email?: string; // オプションプロパティ (?を付ける)
isActive: boolean;
roles: string[];
}
// 型エイリアスでプリミティブ型、結合型、交差型などに名前を付ける
type UserStatus = ‘active’ | ‘inactive’ | ‘pending’;
type Coordinate = {
x: number;
y: number;
};
type Point = Coordinate; // 型エイリアスは既存の型に新しい名前を付けることもできる
// インターフェースと型エイリアスの違い(簡単な使い分けのヒント)
// – オブジェクトやクラスの形状を定義する場合はインターフェースが一般的
// – プリミティブ型、結合型、交差型、タプル、関数型などに名前を付ける場合は型エイリアス
// – インターフェースは同じ名前で複数宣言してマージできる(Declaring Merging)
// – エラーメッセージは型エイリアスの方が読みやすい場合がある
// 例: 型エイリアスでオブジェクトの型を定義
type Product = {
id: number;
name: string;
price: number;
};
“`
Next.jsプロジェクトでは、APIから取得するデータ、コンポーネントのProps、フォームデータの形状などをこれらの機能を使って定義することがよくあります。これらの型定義は、types/
ディレクトリのような場所にまとめて管理するのがおすすめです。
ReactコンポーネントのPropsとStateの型付け
ReactコンポーネントをTypeScriptで書く場合、PropsとStateに型を付けることが非常に重要です。これにより、コンポーネントが期待するデータ構造を明確にし、誤ったPropsが渡されるのを防ぐことができます。
Propsの型付けには、インターフェースや型エイリアスを定義し、それを React.FC
(Function Component) または React.VFC
(Void Function Component) のジェネリクスとして指定するのが一般的です。
“`typescript
// components/Greeting.tsx
import React from ‘react’;
// Propsの型をインターフェースで定義
interface GreetingProps {
name: string; // 必須の文字列プロパティ
age?: number; // オプションの数値プロパティ
}
// React.FCを使ってコンポーネントとPropsの型付け
const Greeting: React.FC
return (
Hello, {name}!
{age &&
You are {age} years old.
}
);
};
export default Greeting;
“`
Stateの型付けは、useState
フックを使っている場合に、フックのジェネリクスとして型を指定します。
“`typescript
// components/Counter.tsx
import React, { useState } from ‘react’;
const Counter: React.FC = () => {
// useStateのジェネリクスに数値型を指定
const [count, setCount] = useState
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount – 1);
};
return (
Count: {count}
);
};
export default Counter;
“`
複雑なStateオブジェクトの場合は、インターフェースや型エイリアスを使って型を定義し、それをuseState
に指定します。
“`typescript
// components/UserForm.tsx
import React, { useState, ChangeEvent, FormEvent } from ‘react’;
// Stateオブジェクトの型を定義
interface FormState {
name: string;
email: string;
isSubscribed: boolean;
}
const UserForm: React.FC = () => {
// Stateの型をFormStateとして指定
const [formData, setFormData] = useState
name: ”,
email: ”,
isSubscribed: false,
});
// 入力フィールド変更時のハンドラ
// イベントオブジェクトの型は ChangeEvent
const handleInputChange = (e: ChangeEvent
const { name, value, type, checked } = e.target;
setFormData({
…formData,
[name]: type === ‘checkbox’ ? checked : value,
});
};
// フォーム送信時のハンドラ
// イベントオブジェクトの型は FormEvent
const handleSubmit = (e: FormEvent
e.preventDefault();
console.log(‘Form submitted:’, formData);
// ここでAPIエンドポイントにデータを送信するなどの処理を行う
};
return (
);
};
export default UserForm;
“`
DOMイベントハンドラの型(ChangeEvent<HTMLInputElement>
など)は、TypeScriptによって提供される標準的な型定義を使用します。VS Codeなどのエディタを使っている場合、これらの型は入力中に自動的に補完されることが多いです。
tsconfig.json
の設定
create-next-app --ts
で生成される tsconfig.json
は、Next.jsとReactプロジェクトに適したデフォルト設定を含んでいます。いくつかの重要なオプションを見てみましょう。
json
{
"compilerOptions": {
"target": "es5", // 出力するJavaScriptのバージョン (Next.jsはこれを無視して自動で最適化)
"lib": ["dom", "dom.iterable", "esnext"], // コンパイルに含める標準ライブラリ
"allowJs": true, // JSファイルのコンパイルを許可
"skipLibCheck": true, // 型定義ファイルの型チェックをスキップ (ビルド速度向上)
"strict": true, // 厳密な型チェックオプションを全て有効化 (推奨)
"forceConsistentCasingInFileNames": true, // ファイル名の大小文字の一貫性を強制
"noEmit": true, // TSからJSへのファイル出力をしない (Next.jsのビルドツールが担当)
"esModuleInterop": true, // CommonJSとES Modules間の互換性を確保
"module": "esnext", // 使用するモジュールシステム
"moduleResolution": "node", // モジュール解決方法
"resolveJsonModule": true, // JSONモジュールのインポートを許可
"isolatedModules": true, // 各ファイルを独立したモジュールとしてコンパイル
"jsx": "preserve", // JSXの処理方法 (Next.jsが処理するためpreserve)
"incremental": true, // インクリメンタルビルドを有効化
"plugins": [
{
"name": "next" // Next.jsプラグインを有効化 (Next.js固有の型チェックや機能)
}
],
"paths": { // モジュール解決のパスエイリアス (jsconfig.jsonでも設定可能)
"@/*": ["./*"] // @/components のようにルートディレクトリからのパスを指定できるようになる
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], // コンパイル対象ファイル
"exclude": ["node_modules"] // コンパイル対象から除外するファイル
}
特に重要なオプション:
"strict": true
: これは強く推奨される設定です。これにより、nullやundefinedに対する厳密なチェック(strictNullChecks
)、暗黙的なany
の防止(noImplicitAny
)など、TypeScriptの主要な厳密チェック機能が全て有効になります。最初は型エラーがたくさん出るかもしれませんが、これらを修正することでコードの信頼性が飛躍的に向上します。"paths"
: これは任意ですが非常に便利です。"@/*": ["./*"]
を設定することで、import Component from '@/components/Component';
のように、プロジェクトのルートディレクトリ(./
)からの絶対パスでモジュールをインポートできるようになります。これにより、相対パス(../../../components/Component
のような記述)による煩雑さを避けられます。設定を有効にするには、VS Codeなどのエディタを再起動する必要がある場合があります。
tsconfig.json
の設定を理解し、特に "strict": true
を活用することで、TypeScriptのメリットを最大限に引き出すことができます。
スタイリング
Next.jsでは、アプリケーションにスタイルを適用する方法としていくつかの選択肢があります。
- CSS Modules: 各CSSファイルが自動的にユニークなクラス名を生成するため、スタイルの衝突を防ぎやすい方法です。Next.jsはCSS Modulesを標準でサポートしています。
- Styled JSX: Reactコンポーネントにスコープ付きCSSを直接記述できるNext.js独自の機能です。
- Global CSS:
pages/_app.tsx
にインポートすることで、アプリケーション全体に適用されるCSSです。 - 外部CSSライブラリ: Tailwind CSS, Styled Components, Emotionなどの人気のCSS-in-JSライブラリやCSSフレームワークも容易に統合できます。
CSS Modulesの例
styles/Home.module.css
のようなファイルを作成し、以下のように記述します。
“`css
/ styles/Home.module.css /
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
“`
ページコンポーネント内でこのCSSファイルをインポートし、クラス名をオブジェクトのプロパティとして使用します。
“`typescript
// pages/index.tsx
import type { NextPage } from ‘next’;
import Head from ‘next/head’;
import Image from ‘next/image’;
import styles from ‘../styles/Home.module.css’; // CSS Modulesをインポート
const Home: NextPage = () => {
return (
// インポートした styles オブジェクトからクラス名を参照
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
{/* ...その他のコンテンツ */}
</main>
{/* ...フッター */}
</div>
);
};
export default Home;
“`
styles
オブジェクトは、CSSファイルで定義されたクラス名に対応するユニークな文字列プロパティを持ちます。これにより、他のコンポーネントで使用されている同じクラス名と衝突することなくスタイルを適用できます。
Styled JSXの例
Styled JSXは、JSX内に<style jsx>
タグを使ってCSSを記述します。デフォルトでスコープ付きスタイルが適用されます。
“`typescript
// components/StyledButton.tsx
import React from ‘react’;
const StyledButton: React.FC = () => {
return (
<>
{/ styled-jsx でスタイルを定義 /}
);
};
export default StyledButton;
“`
<style jsx global>
とすることで、グローバルスタイルを記述することも可能です。
Styled JSXはNext.jsに組み込まれているため、追加の設定なしで使用できます。
Global CSS
アプリケーション全体に適用したい基本的なスタイル(例:bodyのmarginをリセット、共通のフォント設定など)は、styles/globals.css
のようなファイルに記述し、pages/_app.tsx
でインポートします。
“`css
/ styles/globals.css /
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
- {
box-sizing: border-box;
}
“`
pages/_app.tsx
:
“`typescript
import type { AppProps } from ‘next/app’;
import ‘../styles/globals.css’; // ここでインポート
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp;
``
(上記のLayoutの例と組み合わせる場合は、Layoutコンポーネント内で
プロジェクト構造の整理
アプリケーションが大きくなるにつれて、コードを整理することは非常に重要になります。推奨されるプロジェクト構造の例を示します。
my-nextjs-ts-app/
├── components/ # 再利用可能なUIコンポーネント
│ ├── Button.tsx
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── Layout.tsx
│ └── ui/ # より小さな/汎用的なUI要素
│ └── Input.tsx
├── lib/ # バックエンド通信、ユーティリティ関数など
│ ├── api.ts # APIクライアントなど
│ ├── utils.ts # 汎用ユーティリティ関数
│ └── hooks/ # カスタムReactフック
│ └── useFetchData.ts
├── pages/ # ルーティングされるページ
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api/
│ │ └── ...
│ ├── index.tsx
│ ├── about.tsx
│ └── posts/
│ ├── index.tsx
│ └── [id].tsx
├── public/ # 静的ファイル
├── styles/ # スタイルシート
├── types/ # 共通の型定義ファイル
│ ├── index.ts # 型定義をエクスポート
│ ├── Post.ts
│ └── User.ts
├── .next/
├── node_modules/
├── next-env.d.ts
├── package.json
├── README.md
├── tsconfig.json
└── next.config.js
components/
: ボタン、カード、ナビゲーションバーなど、再利用可能なReactコンポーネントを置きます。階層化して整理するのも良いでしょう(例:ui/
ディレクトリ)。lib/
(またはutils/
): アプリケーション全体で使われる非UIロジックを置きます。APIクライアント、データ変換ユーティリティ、ヘルパー関数、カスタムフックなどが含まれます。types/
: アプリケーション全体で共有されるTypeScriptの型定義ファイル(インターフェースや型エイリアス)を集中管理します。例えば、APIレスポンスの型やデータベースモデルの型などを定義します。pages/
: 前述の通り、ルーティングされるページコンポーネントとAPIルートを置きます。
この構造はあくまで一例であり、プロジェクトの規模やチームの好みに応じて調整できます。重要なのは、コードの役割に応じてディレクトリを分け、どこに何があるか分かりやすくすることです。
ビルドとデプロイ
Next.jsアプリケーションは、ビルドすることで本番環境向けに最適化された静的ファイルやサーバーサイドコードを生成します。
プロジェクトのルートディレクトリで、以下のコマンドを実行します。
“`bash
npm run build
または yarn を使う場合:
yarn build
“`
このコマンドは、.next/
ディレクトリ内に本番環境用のビルド成果物を生成します。pages
ディレクトリ内の各ファイルは、getStaticProps
や getServerSideProps
の設定に基づいて、静的HTMLファイル、JSONデータファイル、またはサーバーレス関数として最適化されます。
ビルドが完了したら、以下のコマンドで本番環境をシミュレートしてアプリケーションをローカルで実行できます。
“`bash
npm run start
または yarn を使う場合:
yarn start
“`
これは開発サーバー (npm run dev
) とは異なり、ビルド済みのコードを使用します。
デプロイ
Next.jsアプリケーションのデプロイは非常に簡単です。Next.jsを開発したVercel platform (https://vercel.com/) は、Next.jsアプリケーションに最適化されており、Gitリポジトリと連携させるだけで自動的にビルド・デプロイ・ホスティングを行ってくれます。SSRやAPIルートはサーバーレス関数として自動的にデプロイされます。
Vercel以外にも、Netlify, AWS Amplify, Heroku, Dockerなど、様々なプラットフォームにデプロイ可能です。SSGで生成された静的サイトであれば、S3+CloudFrontやGitHub Pagesといった静的ホスティングサービスにもデプロイできます。
まとめと次のステップ
本記事では、Next.jsとTypeScriptを使ったモダンWebアプリケーション開発の導入として、以下の内容を詳細に解説しました。
- Next.jsとTypeScriptを組み合わせる理由
create-next-app
を使ったプロジェクトのセットアップ方法- ファイルシステムベースのルーティングと動的ルーティング
<Link>
コンポーネントとuseRouter
フックを使ったナビゲーション_app.tsx
を使った共通レイアウトの適用- Next.jsの主要なデータフェッチメソッド (
getServerSideProps
,getStaticProps
,getStaticPaths
) の使い方とTypeScriptによる型付け - APIルートの作成とTypeScriptによる型付け
- TypeScriptの基本的な型、インターフェース、型エイリアス、コンポーネントの型付け
tsconfig.json
の重要な設定- Next.jsでのスタイリング方法(CSS Modules, Styled JSX, Global CSS)
- 推奨されるプロジェクト構造
- アプリケーションのビルドとデプロイ
Next.jsとTypeScriptは、現代のWeb開発において非常に強力なツールです。Next.jsはパフォーマンス、開発効率、SEOなど多くの面でメリットを提供し、TypeScriptは大規模なアプリケーション開発におけるコードの信頼性と保守性を向上させます。この二つを組み合わせることで、より堅牢で高品質なWebアプリケーションを効率的に構築できます。
このガイドはあくまで導入部分です。Next.jsにはさらに多くの機能があります。
次のステップとして、以下のことを学ぶことを推奨します。
- App Router: Next.js 13.4で安定版となった新しいルーターです。サーバーコンポーネント、ストリーミング、ネストされたレイアウトなど、より高度な機能を提供します。Pages Routerとは根本的に異なるアプローチを取るため、別途学習が必要です。(本ガイドはPages Routerを扱いました)
- Middleware: ルーティングの前にリクエストをインターセプトし、処理を行う機能です(認証、リダイレクトなど)。
- Image Optimization:
next/image
コンポーネメントを使った画像の最適化。 - Font Optimization:
next/font
を使ったフォントの最適化。 - Caching: データフェッチにおけるキャッシュ戦略。
- Testing: Next.jsアプリケーションのテスト方法(Jest, React Testing Libraryなど)。
- State Management: Redux, Zustand, Recoilなど、状態管理ライブラリとの連携。
- Error Handling: エラーページのカスタマイズ (
pages/404.tsx
,pages/500.tsx
) やエラー境界。
実際に手を動かして、簡単なToDoリストアプリやブログサイトなど、何か小さなアプリケーションをゼロから構築してみるのが、最も効果的な学習方法です。途中で必ずエラーや疑問にぶつかりますが、それを解決していく過程で理解が深まります。
モダンWeb開発の世界へようこそ!Next.jsとTypeScriptを使いこなして、素晴らしいアプリケーションを創造してください。