getServerSidePropsを用いて、ログインの有無を判断しリダイレクトを実装する

# はじめに

Next.js初心者なりに、getServerSidePropsを用いてページアクセス時のリダイレクトを実装してみました。

## 要件

- ログインしていない場合、認証が必要なページにアクセスした際にログイン画面にリダイレクトする
- 逆に、ログイン済みの場合、ログイン画面を表示しようとした時ホーム画面にリダイレクトする
- ユーザー情報の保持はRecoilを使用。recoil-persistなどのライブラリの使用はなし
- 認証はアクセストークン認証で、トークンはCookieに保存
- トークンの有効性はAPIを叩いて正常なレスポンスが返ってくるかで判断（APIは既製のものを使用した）
    - /accountにリクエストを送ると、tokenが有効であれば以下のようなデータが返ってくる
        
        ```tsx
        {
          "user": {
            "id": 0,
            "name": "string",
            "email": "string",
            "iconImageUrl": "string"
          }
        }
        ```
        
    - tokenが無効なら以下のように返ってくる（空データなだけであって、エラーにならない点に注意）
        
        ```tsx
        {}
        ```
        

# 実装

以下、実装例です。getServerSidePropsの関数は、redirect.tsにまとめて定義し、各ページコンポーネントでredirectToLogin関数とredirectToHomepage関数の必要な方をインポートして使うことを想定しています。

### utils/redirect.ts

```tsx
import { GetServerSideProps } from "next";
import axiosInstance from "./axios";

// 空のオブジェクトかどうかを判定する関数
export const isEmptyObject = (obj: object) => {
  return Object.keys(obj).length === 0;
};

// ログインページへのリダイレクト関数
export const redirectToLogin: GetServerSideProps = async (context) => {
  const token = context.req.cookies.token;

  // そもそもトークンがなければ即リダイレクト
  if (!token) {
    return {
      redirect: {
        destination: "/login",
        permanent: false,
      },
    };
  }

  try {
    // リクエストヘッダーにトークンを添付してリクエストを送信(js-cookieライブラリはサーバーサイドで使えないので、
    // axiosInstanceで自動でtoken付与されない。そのため、ここでtokenを付与している)
    const response = await axiosInstance.get("/account", {
      headers: { Authorization: `Bearer ${token}` },
    });

    // getAccountはTokenを設定せずにリクエストを送った場合、200レスポンスで{}が返ってくる。なので以下のような判定をする。
    // レスポンスが空のオブジェクトなら（トークン無効）、ログインページへリダイレクト
    if (isEmptyObject(response.data)) {
      return {
        redirect: {
          destination: "/login",
          permanent: false,
        },
      };
    }

    // トークンが有効ならユーザーデータをpropsとして渡す
    // これにより、リロード時にrecoilのユーザーデータが消えることを防ぐ
    return {
      props: { user: response.data.user },
    };
  } catch (error) {
    console.error(error);
  }

  return {
    props: {},
  };
};

// ホームページへのリダイレクト関数
export const redirectToHomepage: GetServerSideProps = async (context) => {
  const token = context.req.cookies.token;

  try {
    const response = await axiosInstance.get("/account", {
      headers: { Authorization: `Bearer ${token}` },
    });

    // レスポンスが空のオブジェクトでなければ（トークン有効）、ホームページへリダイレクト
    if (isEmptyObject(response.data) === false) {
      return {
        redirect: {
          destination: "/homepage",
          permanent: false,
        },
      };
    }
  } catch (error) {
    console.error(error);
  }

  return {
    props: {},
  };
};
```

具体的にリダイレクトをしているのは以下の部分

```tsx
      return {
        redirect: {
          destination: "/login",
          permanent: false,
        },
      };
```

`permanent` オプションは、永続的なリダイレクトかどうかを定義する。ページ移転やドメイン引越などでなければ基本はfalseで問題ない。

### ページコンポーネント

```tsx
// homepage.tsx
import React, { useEffect } from "react";
import Header from "@/components/Header";
import { useRecoilState } from "recoil";
import { userState } from "@/recoil/atoms";
import { TOKEN_KEY } from "@/utils/cookieConstants";
import Cookie from "js-cookie";
import { redirectToLogin } from "@/utils/redirect";
import axiosInstance from "@/utils/axios";
import { User } from "@/types";

interface HomePageProps {
  user: User;
}

// getServerSiderPropsから渡されたユーザーデータをPropsとして受け取る
const HomePage: React.FC<HomePageProps> = ({ user: initialUser }) => {
  const [user, setUser] = useRecoilState(userState);
  const token = Cookie.get(TOKEN_KEY);

  // コンポーネントがマウントされた時に、getServerSidePropsからユーザーデータをセットする
  // これは、リロード時にユーザーデータがRecoilから消えてしまうのを防ぐため
  useEffect(() => {
    setUser(initialUser);
  }, [initialUser, setUser]);

  return (
    <>

	〜省略〜

    </>
  );
};

export default HomePage;

// 以下でリダイレクト処理を呼び出す
export const getServerSideProps = redirectToLogin;
```

`export const getServerSideProps = redirectToLogin;` でリダイレクト処理を呼び出している。

getServerSidePropsからは、Propsでユーザーデータも受け取って、useEffectでRecoilのstateにセットしている。

Recoilはrecoil-persistなどのライブラリを使うか、ローカルストレージを使わないとリロード時の状態の永続化ができないため、このような処理にした。


逆に、ログインページではredirectToHomepageを呼び出す。

```tsx
// login.tex
〜 省略 〜

export default Login;

export const getServerSideProps = redirectToHomepage;
```

### 嵌った点

getServerSideProps内では、js-cookieでCookieを参照できない。

どう困ったのかというと、axiosInstance内で自動でCookieからtokenを取得してヘッダーに付与しようとしていたのが、うまく機能しなかったこと。

```tsx
import axios from "axios";
import Cookies from "js-cookie";
import { TOKEN_KEY } from "./cookieConstants";

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_DOMAIN,
  timeout: 1000,
});

// リクエストを行う直前に毎回Authorizationヘッダーを付与する
axiosInstance.interceptors.request.use(
  function (config) {
    const token = Cookies.get(TOKEN_KEY); // トークンを取得(この部分がgetServerSideProps内だとうまく動かない！！）

    // トークンがあればリクエストヘッダーに添付
    if (token) {
      config.headers["Authorization"] = "Bearer " + token;
    }

    return config;
  },
  function (error) {
    // リクエストエラー時の処理
    return Promise.reject(error);
  }
);

export default axiosInstance;
```

それに気づかず、無駄に時間を溶かした…

解決策として、getServerSideProps内で、contextを通してtokenを取得し、以下のようにヘッダーにtokenを付与している。

```ts
    export const redirectToLogin: GetServerSideProps = async (context) => {
    const token = context.req.cookies.token;		

    // リクエストヘッダーにトークンを添付してリクエストを送信(js-cookieライブラリはサーバーサイドで使えないので、
    // axiosInstanceで自動でtoken付与されない。そのため、ここでtokenを付与している)
    const response = await axiosInstance.get("/account", {
      headers: { Authorization: `Bearer ${token}` },
    });

~ 省略　〜
}
```