Next.jsでログインフラグによるリダイレクト機能を実装してみた

# 前書き

Next.jsで自前のログイン機能を作成しようとして情報集めに苦労したのでその備忘録。

前提として、認証処理一式を実装できるライブラリは色々あります。但しSNSとの連携を前提としたモノしかなかった。（僕調べ）

* 個人学習向けの簡単なログイン機能を実装したい
* 自前の画面遷移時のリダイレクト判定処理を実装したい

みたいな方々には、もし良ければこんな実装もあるよっていう参考にしていただければ幸いです。


# 使用技術

* Next.js（メイン）
    * axios：お馴染み、API叩く用に使用
    * nookies：Cookie制御用の外部ライブラリ 
* バックエンドAPI（なんらかのログイン成功フラグが返せればなんでも良い、自分はSpring Bootを使いました）

※ Option

* react-bootstrap（デザインに使用したのみだが下記コードに混ぜてしまったので記述） 

# 機能要件

## 大要件

* ログイン済みか否かで遷移させる画面を制御する

## 小要件

* ログイン済みフラグの取得のために、ログイン画面からIDとパスワードを入力してログイン用バックエンドAPIを叩く（固定値設定して等価判定しているだけの簡単なモノ）
* ログイン済みフラグをクライアント側で保持（Cookieへ）
* Cookieにログイン済みフラグがなければ、遷移時にログイン画面へリダイレクトさせる

# 実装

## ポイント

* `_app.tsx`（Jsなら`_app.jsx`）　で画面制御処理を実装→全画面のコンポーネントで呼ばれるため

## コード

* ログイン画面コンポーネント

```tsx
import Head from 'next/head';
import axios from 'axios';
import { useState } from 'react';
import { Container, Button, Form, Image } from 'react-bootstrap';
import { setCookie } from 'nookies';
import { useRouter } from 'next/router';

interface ILogin {
  userName: string;
  password: string;
}

const initialPayload: ILogin = {
  userName: '',
  password: '',
};

const Login = () => {
  const router = useRouter();

  const [payload, setPayload] = useState<ILogin>(initialPayload);

  const handleChange = (e) => {
    setPayload({
      ...payload,
      [e.target.name]: e.target.value,
    });
  };

  const onClickLogin = () => {
    axios
      .post('/api/login', payload)
      .then((res) => {
        // ログインフラグをクッキーへ、「auth」というキーで登録
        setCookie(null, 'auth', 'true', {
          maxAge: 30 * 24 * 60 * 60, // お好きな期限を
          path: '/',
        });
        router.push('/');
      })
      .catch((e) => {
        console.log('認証エラー');
      });
  };
  return (
    <Container>
      <Head>
        <title>ログイン画面例</title>
      </Head>
      <div className='login-container'>
        <Image
          src='https://placehold.jp/150x150.png'
          roundedCircle
          style={{ marginBottom: '20px' }}
        />
        <Form.Control
          type='text'
          placeholder='User Name'
          name='userName'
          value={payload.userName}
          onChange={handleChange}></Form.Control>
        <Form.Control
          type='password'
          placeholder='Password'
          name='password'
          value={payload.password}
          onChange={handleChange}></Form.Control>
        <Button variant='info' type='button' onClick={onClickLogin}>
          Login
        </Button>
      </div>
    </Container>
  );
};

export default Login;
```

※イメージこんな画面（若干自前CSS足しているので上記コピーだけでは再現されません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609990/ea8486d8-6279-9d25-441f-6d495f37c608.png" width="70%"/>


* `_app.tsx`（メイン）

```tsx
import { NextPageContext } from 'next';
import { AppProps } from 'next/app';
import { parseCookies } from 'nookies';
import { useEffect } from 'react';
import { useRouter } from 'next/router';

const MyApp = ({ Component, pageProps }: AppProps, ctx: NextPageContext) => {
  const router = useRouter();
  const cookies = parseCookies(ctx);

  // 第二引数に空配列を指定してマウント・アンマウント毎（CSRでの各画面遷移時）に呼ばれるようにする
  useEffect(() => {
    // CSR用認証チェック

    router.beforePopState(({ url, as, options }) => {
      // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
      if (url !== '/login' && url !== '/_error') {
        if (typeof cookies.auth === 'undefined') {
          // CSR用リダイレクト処理
          window.location.href = '/login';
          return false;
        }
      }
      return true;
    });
  }, []);

  const component =
    typeof pageProps === 'undefined' ? null : <Component {...pageProps} />;

  return component;
};

MyApp.getInitialProps = async (appContext: any) => {
  // SSR用認証チェック

  const cookies = parseCookies(appContext.ctx);
  // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
  if (
    appContext.ctx.pathname !== '/login' &&
    appContext.ctx.pathname !== '/_error'
  ) {
    if (typeof cookies.auth === 'undefined') {
     // SSR or CSRを判定
      const isServer = typeof window === 'undefined';
      if (isServer) {
        console.log('in ServerSide');
        appContext.ctx.res.statusCode = 302;
        appContext.ctx.res.setHeader('Location', '/login');
        return {};
      } else {
        console.log('in ClientSide');
      }
    }
  }
  return {
    pageProps: {
      ...(appContext.Component.getInitialProps
        ? await appContext.Component.getInitialProps(appContext.ctx)
        : {}),
      pathname: appContext.ctx.pathname,
    },
  };
};

export default MyApp;

```

# 後書き

Next（Nuxtも）はSSRとCSRそれぞれを考慮する必要があるからめんどい、、けど良い勉強になった。
Nextはほぼ英語記事しかないので日本語の参考記事も増えたらいいなあ（やっぱり日本ではNuxt・・・？）

# 参考

* [【React】useEffectの第２引数って？](https://qiita.com/k-penguin-sato/items/9373d87c57da3b74a9e6)
* [Next.jsでcookieをシンプルに扱うことができるライブラリ nookies を紹介](https://qiita.com/maedakei0817/items/1c5efc9d9f45c73dcbe0)

※リダイレクト周りはほぼ英語記事参考にしましたが散り散りで集められませんでしたすみません・・。