Next.jsで使える4つの画面表示方法と合わせ技

Next.jsには画面の表示方法が4通りあります。
1. SSG(Static Site Generator):静的サイト生成
2. SSR(Server Side Rendering): サーバーサイドレンダリング
3. ISR(Incremental Static Regeneration): インクリメンタル静的再生性
4. CSR(Client Side Rendering): クライアントサイドレンダリング

それぞれ実装方法が違っていたりCORSのエラーが発生したり挙動に異なるので、ここでまとめようと思います。
CORSエラーが発生した時もNext.jsの機能で解決できるのでそれも紹介しています。

(データ取得時の挙動の確認がメインなのでNext.jsのコードの書き方がおかしい箇所があるかもしれません。)

そしてこれらの画面表示方法を組み合わせることで高速な画面表示も可能になるので紹介します。

# 前準備
前準備として`http://localhost:3000`で動くNext.jsのフロントエンドサーバーと、`http://localhost:8080`で動くAPIサーバーを用意します。
APIサーバーは、`http://localhost:8080/users`にアクセスすると以下のような情報が取得できるものとします。
random-userのemailアドレスはAPIが叩かれる度にランダムなメールアドレスが返されるものとしています。
特にCORSの設定などはしておりません。
```
[
    {"username": "user1", "email": "user1@example.com"},
    {"username": "random-user", "email": random_email},
]
```
使用するNext.jsのプロジェクトと、APIのプロジェクトは以下のものを使用しています。
https://github.com/y-p-e/fastapi_test
https://github.com/y-p-e/nextjs-fetch-test
 
# 1. SSGによるユーザー一覧画面表示
最初はSSGでユーザー一覧画面を表示する方法になります。
SSGの使い所は、ブログ記事など頻繁にデータが更新されないような画面に対して有効かと思います。

SSGはデータ取得に`getStaticProps`を使います。
実装は以下になります。
```
import type {
	GetStaticProps,
  InferGetStaticPropsType,
  NextPage,
} from 'next'

import getUsers, {ApiContext, User} from '../../services/users/get-users'

type UserPageProps = InferGetStaticPropsType<typeof getStaticProps>

const SSG: NextPage<UserPageProps> = ({users}: UserPageProps) => {
	return (
		<div>
			<h1>SSGによるユーザー一覧情報取得</h1>
			{users.map((user: User) => (
				<>
					<p>{`username: ${user.username}`}</p>
					<p>{`email: ${user.email}`}</p>
				</>
			))}
		</div>
	)
}

export const getStaticProps: GetStaticProps = async () => {
  const context: ApiContext = {apiRootUrl: 'http://localhost:8080'}
	const users = await getUsers(context)

  return {
    props: {
      users,
    },
  }
}

export default SSG
```

SSGはビルド時にデータを取得するので`npm run build`を実行します。
```
nextjs-fetch-test % npm run build

> nextjs-fetch-test@0.1.0 build
> next build

info  - SWC minify release candidate enabled. https://nextjs.link/swcmin
info  - Linting and checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (4/4)
info  - Finalizing page optimization  

Route (pages)                              Size     First Load JS
┌ ○ /                                      5.41 kB        83.3 kB
├   └ css/ae0e3e027412e072.css             707 B
├   /_app                                  0 B            77.9 kB
├ ○ /404                                   186 B          78.1 kB
├ λ /api/hello                             0 B            77.9 kB
└ ● /users/ssg                             413 B          78.3 kB
+ First Load JS shared by all              78.1 kB
  ├ chunks/framework-9b5d6ec4444c80fa.js   45.7 kB
  ├ chunks/main-3123a443c688934f.js        30.9 kB
  ├ chunks/pages/_app-deb173bd80cbaa92.js  499 B
  ├ chunks/webpack-7ee66019f7f6d30f.js     755 B
  └ css/ab44ce7add5c3d11.css               247 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
```
`● /users/ssg`となっているので、/users/ssgがSSGで作成されたことを確認できます。

`npm run start`でサーバー起動。

画面は以下のようになります。
![ssg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/bcd42b06-96b8-05b7-c44a-202ca99262f6.png)

SSGはビルド時にデータを取得するので画面を更新してもrandom-userのemailは変更されません。
もし`npm run dev`で起動した場合は、リクエストがある度に`getStaticProps`が実行されるのでrandom-userのemailは毎回表示が変わります。


# 2. SSRによるユーザー一覧画面表示
次はSSRでユーザー一覧画面を表示する方法になります。
SSRの使いどころはデータのリアルタイム性が求められるような画面かと思います。

SSRはデータ取得に`getServerSidecProps`を使います。
実装は以下になります。
```
import type {
	GetServerSideProps,
	InferGetServerSidePropsType,
  NextPage,
} from 'next'

import getUsers, {ApiContext, User} from '../../services/users/get-users'

type UserPageProps = InferGetServerSidePropsType<typeof getServerSideProps>

const SSR: NextPage<UserPageProps> = ({users}: UserPageProps) => {
	return (
		<div>
			<h1>SSRによるユーザー一覧情報取得</h1>
			{users.map((user: User) => (
				<>
					<p>{`username: ${user.username}`}</p>
					<p>{`email: ${user.email}`}</p>
				</>
			))}
		</div>
	)
}

export const getServerSideProps: GetServerSideProps = async () => {
  const context: ApiContext = {apiRootUrl: 'http://localhost:8080'}
	const users = await getUsers(context)

  return {
    props: {
      users,
    },
  }
}

export default SSR
```

`npm run build`を実行します。
```
nextjs-fetch-test % npm run build

> nextjs-fetch-test@0.1.0 build
> next build

info  - SWC minify release candidate enabled. https://nextjs.link/swcmin
info  - Linting and checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (4/4)
info  - Finalizing page optimization  

Route (pages)                              Size     First Load JS
┌ ○ /                                      5.41 kB        83.3 kB
├   └ css/ae0e3e027412e072.css             707 B
├   /_app                                  0 B            77.9 kB
├ ○ /404                                   186 B          78.1 kB
├ λ /api/hello                             0 B            77.9 kB
├ ● /users/ssg                             413 B          78.3 kB
└ λ /users/ssr                             412 B          78.3 kB
+ First Load JS shared by all              78.1 kB
  ├ chunks/framework-9b5d6ec4444c80fa.js   45.7 kB
  ├ chunks/main-3123a443c688934f.js        30.9 kB
  ├ chunks/pages/_app-deb173bd80cbaa92.js  499 B
  ├ chunks/webpack-7ee66019f7f6d30f.js     755 B
  └ css/ab44ce7add5c3d11.css               247 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
```
`λ /users/ssr`となっており、/users/ssrがSSRで作成されたことを確認できます。

`npm run start`でサーバー起動。
画面は以下のような感じになっています。
![ssr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/a4ff0498-afd5-5693-4891-d6476515e54c.png)

更新する度にrandom-userのemailが変更されていることが確認できます。
これはSSRはアクセスする度にサーバーでデータを取得しページを描画するためです。

# 3. ISRによるユーザー一覧画面表示
次はISRでユーザー一覧画面を表示する方法になります。
ISRは頻繁にデータは更新されないけど、ある程度の時間で最新情報を取得し画面を更新したい時に利用できます。

ISRはSSGと同様でデータ取得に`getStaticProps`を使います。
異なるのは`revalidate`で指定した期間で静的ページを更新することです。
実装は以下になります。
```
import type {
	GetStaticProps,
  InferGetStaticPropsType,
  NextPage,
} from 'next'

import getUsers, {ApiContext, User} from '../../services/users/get-users'

type UserPageProps = InferGetStaticPropsType<typeof getStaticProps>

const ISR: NextPage<UserPageProps> = ({users}: UserPageProps) => {
	return (
		<div>
			<h1>ISRによるユーザー一覧情報取得</h1>
			<h2>10間隔で静的ページを更新します</h2>
			{users.map((user: User) => (
				<>
					<p>{`username: ${user.username}`}</p>
					<p>{`email: ${user.email}`}</p>
				</>
			))}
		</div>
	)
}

export const getStaticProps: GetStaticProps = async () => {
  const context: ApiContext = {apiRootUrl: 'http://localhost:8080'}
	const users = await getUsers(context)

  return {
    props: {
      users,
    },
		revalidate: 10
  }
}

export default ISR
```
今回は10秒間隔で静的ページを更新する設定にしています。

`npm run build`を実行します。
```
nextjs-fetch-test % npm run build

info  - SWC minify release candidate enabled. https://nextjs.link/swcmin
info  - Linting and checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (5/5)
info  - Finalizing page optimization  

Route (pages)                              Size     First Load JS
┌ ○ /                                      5.41 kB        83.3 kB
├   └ css/ae0e3e027412e072.css             707 B
├   /_app                                  0 B            77.9 kB
├ ○ /404                                   186 B          78.1 kB
├ λ /api/hello                             0 B            77.9 kB
├ ● /users/isr (ISR: 10 Seconds)           473 B          78.3 kB
├ ● /users/ssg                             413 B          78.3 kB
└ λ /users/ssr                             412 B          78.3 kB
+ First Load JS shared by all              78.1 kB
  ├ chunks/framework-9b5d6ec4444c80fa.js   45.7 kB
  ├ chunks/main-3123a443c688934f.js        30.9 kB
  ├ chunks/pages/_app-deb173bd80cbaa92.js  499 B
  ├ chunks/webpack-7ee66019f7f6d30f.js     755 B
  └ css/ab44ce7add5c3d11.css               247 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)
```

`● /users/isr (ISR: 10 Seconds)  `と表示されているのでISRで作成されています。

`npm run start`でサーバー起動します。
実際の画面は以下のようになります。
![isr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/0cfbb0eb-b3ca-7f6e-3cfa-1c3986285972.png)

表示された画面を更新してもrandom-userのemailは変更されませんが、10秒たつと静的ページを更新するのでrandom-userのemailが変更されていることを確認できます。

# 4. CSRによるユーザー一覧画面表示
次はCSRでユーザー一覧画面を表示する方法になります。
今回は`useSWR`を使用してデータを取得します。
実装は以下になります。
```
import type {
  NextPage,
} from 'next'

import {User} from '../../services/users/get-users'
import useSWR from 'swr'
import { fetcher } from "../../utils"


const CSR: NextPage = () => {
	const {data, error, mutate} = useSWR<User[]>('http://localhost:8080/users', fetcher, {shouldRetryOnError: false})
	console.log(data)
	const onClickUser = () => {
		mutate(data, false)
	}

	if (data === undefined) {
		return (
			<div>
				<h1>CSRによるユーザー一覧情報取得</h1>
				<button onClick={onClickUser}>ユーザー情報取得</button>
			</div>
		)
	}
	
	return (
		<div>
			<h1>CSRによるユーザー一覧情報取得</h1>
			<button onClick={onClickUser}>ユーザー情報取得</button>
			{	data.map((user: User) => (
					<>
						<p>{`username: ${user.username}`}</p>
						<p>{`email: ${user.email}`}</p>
					</>
				))
			}
		</div>
	)
}

export default CSR
```
画面更新時、ボタン押下時にデータを取得しユーザー情報を表示するコンポーネントを更新する設定にしています。

しかしこの設定で画面を開くとユーザー情報が表示されていません。
![csr_cors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/9e92624a-e233-8048-9d15-d2e920116ec3.png)


コンソールには下記のようなエラーが表示されます。
![cors_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/7e081b9b-2042-fc2f-152f-0efec913f906.png)

これは、next.jsで作成された画面がローカル環境では`http://localhost:3000`であるのに対して、APIサーバーが`http://localhost:8080`となっておりオリジンが異なることによるCORSエラーになります。

### CORSエラーを回避する方法
手取り早くCORSエラーを回避する方法にNext.jsのRewrites機能が使えます。

まずは画面の実装でユーザー情報を取得するAPIのURLを下記のように`/api/users/csr`に変更し、Next.jsのオリジンと同一にします。
```
const {data, error, mutate} = useSWR<User[]>('/api/users/csr', fetcher, {shouldRetryOnError: false})
```

次に`next.config.js`を下記のように記載し、`/api/users/csr`のアクセスを`http://localhost:8080/users`に変更するように設定します。
```
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  async rewrites() {
    return [
      {
        source: '/api/users/csr',
        destination: 'http://localhost:8080/users',
      },
    ]
  },
}

module.exports = nextConfig

```
この状態で画面を開くと下記のように正常に表示できます。さらに、ボタン押下でデータを取得し表示を更新もできます。
![csr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/21caec4e-6dfc-8239-755e-38fe9148b333.png)

他にもCORSエラーを回避する方法としてnginxを使用して制御する方法などもありますが、これは別の記事でまとめようと思います。


# ISRとCSRの組み合わせによる画面表示
上記で紹介した画面表示方法は組み合わせることができます。
そこでここでは一例としてISRとCSRを組み合わせた使い方を紹介します。
この組み合わせの良いところは、まず静的ページで画面を表示した後に必要な箇所をCSRで更新できることです。
こうすることで高速な画面表示を実現することができます。
実装例は以下のような感じになります。
```
import type {
	GetStaticProps,
  InferGetStaticPropsType,
  NextPage,
} from 'next'
import CsrUserList from '../../components/csrUser'
import NotCsrUserList from '../../components/notCsrUser'
import { useState } from 'react'
import getUsers, {ApiContext, User} from '../../services/users/get-users'

type UserPageProps = InferGetStaticPropsType<typeof getStaticProps>

const UserList = (props:any) => {
  return (
		<>
			<p>{`username: ${props.user.username}`}</p>
			<p>{`email: ${props.user.email}`}</p>
		</>
  );
}

const ISRCSR: NextPage<UserPageProps> = ({users}: UserPageProps) => {
	const [isRendering, setIsRendering] = useState(false)
	return (
		<div>
			<h1>ISRによるユーザー一覧情報取得</h1>
			<h2>10間隔で静的ページを更新します</h2>
			<div>
				{	users.map((user: User, index: number) => (
					<UserList user={user} key={index}/>
					))
				}
			</div>
			<button onClick={()=>setIsRendering(true)}>CSRユーザー情報取得</button>
			{isRendering ? <CsrUserList /> : <NotCsrUserList />}
		</div>
	)
}

export const getStaticProps: GetStaticProps = async () => {
  const context: ApiContext = {apiRootUrl: 'http://localhost:8080'}
	const users = await getUsers(context)

  return {
    props: {
      users,
    },
		revalidate: 60
  }
}

export default ISRCSR
```
初期表示用の子コンポーネントは以下を用意しました。
```
const NotCsrUserList = () => {
	return <div></div>
}

export default NotCsrUserList
```

ボタンを押下しCSRでレンダリングするためのコンポーネントは以下のようにしました。
```
import { User } from '../services/users/get-users'
import useSWR from 'swr'
import { fetcher } from '../utils'

const UserList = (props:any) => {
  return (
		<>
			<p>{`username: ${props.user.username}`}</p>
			<p>{`email: ${props.user.email}`}</p>
		</>
  );
}

const CsrUserList = () => {
	const {data, error} = useSWR<User[]>('/api/users/csr', fetcher, {shouldRetryOnError: false})
	if (data === undefined) {
		return (
			<div>
				<h1>CSRによるユーザー一覧情報取得</h1>
			</div>
		)
	}

	return (
		<div>
			<h1>CSRによるユーザー一覧情報取得</h1>
			<div>
			{	data.map((user: User, index: number) => (
				<UserList user={user} key={index}/>
				))
			}
			</div>
		</div>
	)
}

export default CsrUserList
```

これによりまずは静的ページで次のような画面を表示します。
![isr_csr_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/ff5c4d5d-675c-d126-ea79-f553c8e8a874.png)

そしてボタンを押下しCSRでデータを取得しコンポーネントをレンダリング。
![isr_csr_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2283175/3b1ee8b6-6278-516f-5ccd-7364ec684779.png)

これができることで静的ページで画面を表示し、更新があった箇所など必要なところだけクライアント側で更新できるので高速に画面を表示することが可能になります。


# まとめ
Next.jsには画面を表示する方法をまとめてみました。これらの使い分けは、データの更新頻度やリアルタイム性などを考えて選定していくのが良いと感じました。
また、CSRの場合はCORSの問題も発生するのでそのあたりも気を付けるポイントかなと思います。
これによってどういうネットワーク構成にするのか、サーバー側でCORS回避の設定を入れるかなど設計が変わるので理解は必須だなと思いました。