LoginSignup
8
7

More than 1 year has passed since last update.

Next.js + Supabase でログイン周りの機能を実装する

Last updated at Posted at 2023-03-02

今回の記事では、Next.jsとSupabaseを使って、

・ユーザー登録
・ログイン
・ログイン中のユーザーの表示
・ログアウト
・パスワード再設定

の機能を実装する方法を解説していきます。

また、上記の機能をNext.jsとFirebaseを使って実装する方法もこちらの記事で解説しています。

開発環境

  • macOS Catalina 10.15.7
  • Next.js
  • Supabase
  • Reactstrap

※Next.jsはJavaScriptを使っています。

プロジェクト作成

Next.jsのプロジェクトを作成します。ターミナルで以下のコマンドを入力してください。

npx create-next-app

プロジェクトの作成が終わったら、以下のコマンドを実行します。

npm install
npm run dev

ここまでできたら一旦、http://localhost:3000/ にアクセスし、Next.jsの初期画面が表示されることを確認してください。

Reactstrapのインストール

今回は画面のデザインを整えるためにReactstrapを使います。まず以下のコマンドでReactstrapとBootstrapをインストールしてください。

npm install reactstrap react react-dom
npm install --save bootstrap

インストールしたら_app.jsを開いて、以下のように編集します。

pages/_app.js
// globals.cssをコメントアウトする
// import '../styles/globals.css'

// Bootstrapを読み込む
import 'bootstrap/dist/css/bootstrap.min.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

これでReactstrapが使えるようになります。

Supabaseのインストール

Supabaseと認証機能の作成に必要なライブラリをインストールします。

まずターミナルで以下のコマンドを入力してSupabaseのパッケージをインストールします。

npm install @supabase/supabase-js

続いて、以下のコマンドもターミナルで実行し、認証機能に必要なライブラリもインストールしておきます。

npm install @supabase/auth-helpers-nextjs
npm install @supabase/auth-helpers-react

Supabaseの設定

次はSupabaseの認証機能を使うための設定をしていきます。

まずはSupabaseのサイトを開いて「Sign in」を選択し、Supabaseの管理画面にログインしてください。※まだSupabaseのアカウントを作成していない場合は「Start your project」のところからアカウントを作成してください。
スクリーンショット 2023-01-30 16.51.24.png

ログインしたら管理画面にある「New project」をクリックします。
スクリーンショット 2023-01-30 16.52.10.png

するとorganizationを選択するポップアップが出てきますので、すでに作成されているorganizationを選択します。
スクリーンショット 2023-01-30 16.52.39.png

新しいプロジェクトを作成画面が表示されるので、プロジェクト名とデータベースのパスワードを入力し、Regionを「Northeast Asia (Tokyo)」に変更して「Create new project」をクリックします。
スクリーンショット 2023-01-30 16.54.37.png

プロジェクトの管理画面に遷移されます。この時点でまだプロジェクト名の横に「Setting up project」の表示が出ている場合は、表示が消えるまで待ちます。
スクリーンショット 2023-01-31 10.png

「Setting up project」の表示が消えたら、サイドバーにある「Authentication」を選択します。
スクリーンショット 2023-01-30 16.57.37.png

「Authentication」の画面を開いたら「Providers」を選択します。
スクリーンショット 2023-01-30 16.57.52.png

「email」のところを開きます。
スクリーンショット 2023-01-30 16.59.23.png

「Confirm email」をオフにして「save」をクリックしてください。今回はメールアドレスの認証を行わなくてもログインできるようにするためにこちらはオフにしておきます。
スクリーンショット 2023-01-30 17.00.01.png

これでログイン機能を実装するためのSupabaseの管理画面上での設定は完了です。

.envファイルに環境変数を設定する

Next.jsのプロジェクトのルートディレクトリに.envファイルを作成します。

次にSupabaseのプロジェクトの管理画面を開き、サイドバーにある「Project Setting」を選択します。
スクリーンショット 2023-01-31 11.31.43.png

「API」の画面を開き、

  • Project URL
  • Project API keys

の値をコピーします。
スクリーンショット 2023-01-31 11.png

コピーした値を.envに環境変数として設定します。

.env
NEXT_PUBLIC_SUPABASE_URL=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx
NEXT_PUBLIC_SUPABASE_API_KEY=xxxxxxxxxxxxx_xxxxxxxxx_xxxxxxxxxxxxx

Supabaseの初期化

まずSupabaseの初期化を行うためのファイルを用意します。プロジェクトのルートディレクトリにutilsというフォルダを新しく作成し、その中にsupabase.jsというファイルを作成します。

supabase.jsの中身は以下のように書いてください。

utils/supabase.js
import { createClient } from '@supabase/supabase-js'

// supabaseの初期化を行う
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_API_KEY
)

ここからは実際にNext.jsに認証関連の処理を書いていきます。

ユーザー登録

まずはユーザー登録機能から実装します。

プロジェクトのpagesディレクトリの中にregister.jsというファイルを新しく作成し、中身を以下のようにしてください。

pages/register.js
import styles from '../styles/Home.module.css'
// 現時点で使わないものもあるが今後のことを考えて入れておく
import { Col, Container, Form, FormGroup, Input, Label, Row, Button } from "reactstrap";
import { useState } from 'react';

// supabase
import { supabase } from '../utils/supabase';


export default function Register() {
  // useStateでユーザーが入力したメールアドレスとパスワードをemailとpasswordに格納する
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // supabaseのユーザー登録の関数
  const doRegister =  async () => {
    // supabaseで用意されているユーザー登録の関数
    const { data, error } = await supabase.auth.signUp({ email, password })
    if (error) throw new Error(error.message)
    console.log(data)
  }

  return (
    // Home.module.cssでcardクラスに適用されているCCSを、このdivタグに適用する
    <div className={styles.card}>
      <h1>新規登録</h1>
      <div>
        <Form>
            <FormGroup>
              <Label>
                メールアドレス
              </Label>
              <Input
                type="email"
                name="email"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をemailに入れる
                onChange={(e) => setEmail(e.target.value)}
              />
            </FormGroup>
            <FormGroup>
              <Label>
                パスワード
              </Label>
              <Input
                type="password"
                name="password"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をpasswordに入れる
                onChange={(e) => setPassword(e.target.value)}
              />
            </FormGroup>
            <Button
                style={{ width: 220 }}
                color="primary"
                // 登録ボタンがクリックされたとき関数が実行されるようにする
                onClick={()=>{
                  doRegister();
                }}
              >
              登録
            </Button>
        </Form>
      </div>
    </div>
  )
}

ユーザーが入力したメールアドレスとパスワードをuseStateを使って受け取り、ユーザー登録のボタンがクリックされたときに、Supabaseで用意されているユーザー登録の関数が実行されるという処理になっています。

Register.jsの中身を書いたら、実際に http://localhost:3000/register にアクセスし、フォームからユーザーの登録ができるかどうかを確認してみましょう。
スクリーンショット 2023-01-31 16のコピー.png

フォームを入力した後にSupabaseの管理画面でAuthenticationのUsersを見て、先ほどRegisterのページから入力したメールアドレスが登録されているかどうかを確認します。

こちらのようにメールアドレスが登録されていれば問題ありません。
スクリーンショット 2023-01-31 16.29.28.png

ログイン

ユーザー登録と同じようにログイン機能も作成していきます。pagesディレクトリにlogin.jsというファイルを作成して中身を以下のように書いてください。

pages/login.js
import styles from '../styles/Home.module.css'
// 現時点で使わないものもあるが今後のことを考えて入れておく
import { Col, Container, Form, FormGroup, Input, Label, Row, Button } from "reactstrap";
import { useEffect, useState } from 'react';

// supabase
import { supabase } from '../utils/supabase';


export default function Register() {
  // useStateでユーザーが入力したメールアドレスとパスワードをemailとpasswordに格納する
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // ログインの関数
  const doLogin =  async () => {
    // supabaseで用意されているログインの関数
    const { data, error } = await supabase.auth.signInWithPassword({ email, password })
    if (error) throw new Error(error.message)
    console.log(data)
  }

  return (
    // Home.module.cssでcardクラスに適用されているCCSを、このdivタグに適用する
    <div className={styles.card}>
      <h1>ログイン</h1>
      <div>
        <Form>
            <FormGroup>
              <Label>
                メールアドレス
              </Label>
              <Input
                type="email"
                name="email"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をemailに入れる
                onChange={(e) => setEmail(e.target.value)}
              />
            </FormGroup>
            <FormGroup>
              <Label>
                パスワード
              </Label>
              <Input
                type="password"
                name="password"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をpasswordに入れる
                onChange={(e) => setPassword(e.target.value)}
              />
            </FormGroup>
            <Button
                style={{ width: 220 }}
                color="primary"
                // 登録ボタンがクリックされたとき関数が実行されるようにする
                onClick={()=>{
                  doLogin();
                }}
              >
              ログイン
            </Button>
        </Form>
      </div>
    </div>
  )
}

ログインの処理はユーザー登録の処理とほぼ同じです。useStateでユーザーが入力したメールアドレスとパスワードを取得して、ログインボタンがクリックされたときにsupabaseのログイン関数が実行されるような仕組みになっています。

ログインしているユーザーの表示

続いてはログインしているユーザーの情報を画面に表示する処理を作成していきます。ヘッダーコンポーネントを作成し、そこにログインしているユーザーのメールアドレスが表示されるような処理を書いていきます。

プロジェクトのルートディレクトリにcomponentsフォルダを作成します。その中にHeader.jsというファイルを作成します。Header.jsの中身は以下のように書いてください。

components/Header.js
import { Button } from 'reactstrap';
import { useEffect, useState } from 'react';

// supabaseをインポート
import { supabase } from '../utils/supabase';

const Header = () => {
  const [currentUser, setcurrentUser] = useState('');

    // 現在ログインしているユーザーを取得する処理
  const getCurrentUser = async () => {
    // ログインのセッションを取得する処理
    const { data } = await supabase.auth.getSession()
    // セッションがあるときだけ現在ログインしているユーザーを取得する
    if (data.session !== null) {
      // supabaseに用意されている現在ログインしているユーザーを取得する関数
      const { data: { user } } = await supabase.auth.getUser()
      // currentUserにユーザーのメールアドレスを格納
      setcurrentUser(user.email)
    }
  }

  // HeaderコンポーネントがレンダリングされたときにgetCurrentUser関数が実行される
  useEffect(()=>{
    getCurrentUser()
  },[])

  return (
    <div style={{ padding: "1rem" }} >
      { currentUser ? (
        // サーバーサイドとクライアントサイドでレンダーされる内容が違うときにエラーがでないようにする
        <div suppressHydrationWarning={true}>
          <div style={{ paddingBottom: "1rem" }}>{ currentUser } でログインしています</div>
        </div>
      ):(
        <div suppressHydrationWarning={true}>ログインしていません</div>
      )}
    </div>
  );
}

export default Header;

こちらの部分が現在ログインしているユーザーを取得するための処理です。

components/Header.js
    // 現在ログインしているユーザーを取得する処理
  const getCurrentUser = async () => {
    // ログインのセッションを取得する処理
    const { data } = await supabase.auth.getSession()
    // セッションがあるときだけ現在ログインしているユーザーを取得する
    if (data.session !== null) {
      // supabaseに用意されている現在ログインしているユーザーを取得する関数
      const { data: { user } } = await supabase.auth.getUser()
      // currentUserにユーザーのメールアドレスを格納
      setcurrentUser(user.email)
    }
  }

この関数は、ログインしていないときに実行されるとコンソールにエラーが出てしまうので、ログインのセッションがある場合だけ実行されるようにしておきます。

では、このヘッダーコンポーネントをログインページにインポートして、ログインしている場合はユーザーのメールアドレス、ログインしていない場合は「ログインしていません」と表示されるようにしていきます。

pagesディレクトリにあるlogin.jsを以下のように編集してください。

pages/login.js
import styles from '../styles/Home.module.css'
// 現時点で使わないものもあるが今後のことを考えて入れておく
import { Col, Container, Form, FormGroup, Input, Label, Row, Button } from "reactstrap";
import { useEffect, useState } from 'react';
// supabase
import { supabase } from '../utils/supabase';

// ヘッダーコンポーネントをインポート
import  Header from '../components/Header';
// useRouterをインポート
import { useRouter } from 'next/router';

export default function Register() {
  // useStateでユーザーが入力したメールアドレスとパスワードをemailとpasswordに格納する
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const router = useRouter();

  // supabaseのユーザー登録の関数
  const doLogin =  async () => {
    // supabaseで用意されているユーザー登録の関数
    const { data, error } = await supabase.auth.signInWithPassword({ email, password })
    if (error) throw new Error(error.message)
    console.log(data)
    // ログインを反映させるためにリロードさせる
    router.reload()
  }

  return (
    // Home.module.cssでcardクラスに適用されているCCSを、このdivタグに適用する
    <div className={styles.card}>
      <h1>ログイン</h1>
      <Header/>
      <div>
        <Form>
            <FormGroup>
              <Label>
                メールアドレス
              </Label>
              <Input
                type="email"
                name="email"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をemailに入れる
                onChange={(e) => setEmail(e.target.value)}
              />
            </FormGroup>
            <FormGroup>
              <Label>
                パスワード
              </Label>
              <Input
                type="password"
                name="password"
                style={{ height: 50, fontSize: "1.2rem" }}
                // onChangeでユーザーが入力した値を取得し、その値をpasswordに入れる
                onChange={(e) => setPassword(e.target.value)}
              />
            </FormGroup>
            <Button
                style={{ width: 220 }}
                color="primary"
                // 登録ボタンがクリックされたとき関数が実行されるようにする
                onClick={()=>{
                  doLogin();
                }}
              >
              ログイン
            </Button>
        </Form>
      </div>
    </div>
  )
}

ヘッダーコンポーネントを読み込んで表示させるための記述と、ログインしたあとにヘッダーコンポーネントにあるユーザーの情報を取得するgetCurrentUser()が実行されるようにするためのリロードの処理を追加しています。

ここまでできたら実際に http://localhost:3000/login にアクセスしてログインを行い、ログインしているユーザーのメールアドレスが表示されるかどうかを確認してみましょう。
スクリーンショット 2023-02-01 14.png

ログアウト

では、先ほど作成したヘッダーコンポーネントにログアウトの処理を追加していきます。Header.jsの内容を以下のように編集してください。

components/Header.js
import { Button } from 'reactstrap';
import { useEffect, useState } from 'react';

// supabaseをインポート
import { supabase } from '../utils/supabase';

// useRouterをインポート
import { useRouter } from 'next/router';

const Header = () => {
  const [currentUser, setcurrentUser] = useState('');
  // routerを使うための記述
  const router = useRouter();

    // 現在ログインしているユーザーを取得する処理
  const getCurrentUser = async () => {
    // ログインのセッションを取得する処理
    const { data } = await supabase.auth.getSession()
    // セッションがあるときだけ現在ログインしているユーザーを取得する
    if (data.session !== null) {
      // supabaseに用意されている現在ログインしているユーザーを取得する関数
      const { data: { user } } = await supabase.auth.getUser()
      // currentUserにユーザーのメールアドレスを格納
      setcurrentUser(user.email)
    }
  }

  // HeaderコンポーネントがレンダリングされたときにgetCurrentUser関数が実行される
  useEffect(()=>{
    getCurrentUser()
  },[])

  // ログアウトの処理を追加
  const doLogout = async () => {
    // supabaseに用意されているログアウトの関数
    const { error } = await supabase.auth.signOut()
    if (error) throw new Error(error.message)
    // ログアウトを反映させるためにリロードさせる
    router.reload()
  }

  // ログアウトボタンも追加
  return (
    <div style={{ padding: "1rem" }} >
      { currentUser ? (
        // サーバーサイドとクライアントサイドでレンダーされる内容が違うときにエラーがでないようにする
        <div suppressHydrationWarning={true}>
          <div style={{ paddingBottom: "1rem" }}>{ currentUser } でログインしています</div>
          <div>
            <Button onClick={()=>{
              doLogout();
            }} >
              ログアウト
            </Button>
          </div>
        </div>
      ):(
        <div suppressHydrationWarning={true}>ログインしていません</div>
      )}
    </div>
  );
}

export default Header;

ログアウトに関してもSupabaseにログアウト関数が用意されているので、そちらを使えばログアウトの機能を実装できます。

また、ログアウトしたときにログイン状態を監視するgetCurrentUser()が呼び出されるように、router.reload()を使ってページをリロードさせています。

ここまで実装を行えば、ログインしている状態でヘッダーにログアウトボタンが表示されるので、ログアウトボタンを押して実際にログアウトできるかどうかを確認してみましょう。
スクリーンショット 2023-02-01 14.png

パスワード再設定

最後にパスワード再設定機能を実装していきます。

パスワード再設定機能は

  • パスワード再設定メールの送信する処理
  • パスワード再設定画面でのパスワードの再設定をする処理

の2つに大きくわかれています。まずは、パスワード再設定のためのメールをユーザーに送信するための処理を作成していきます。

プロジェクトのpagesディレクトリにforgot_password.jsというファイルを作成します。これがパスワードを再設定できるページのリンクが記載されたメールをユーザーのメールアドレスに送信するためのページになります。

forgot_password.jsの中身は以下のように書きます。

pages/forgot_password.js
import styles from '../styles/Home.module.css'
import { Col, Container, Form, FormGroup, Input, Label, Row, Button } from "reactstrap";
import { useState } from 'react';

// supabase
import { supabase } from '../utils/supabase';

export default function Login() {
  const [email, setEmail] = useState('');

  // 送信ボタンがクリックされるとdoResetEmail関数が実行される
  const sendResetEmail = async () => {
    // supabaseで用意されているパスワード再設定のメールを送信するための関数
    const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
      // 後ほどここにパスワード再設定画面のリンクを設定します
      redirectTo: '',
    })
    if (error) throw new Error(error.message)
    console.log(data)
    // メールが送信されたことをわかりやすくするためのアラート
    alert("メールを送信しました。")
  }

  return (
    <div className={styles.card}>
      <h1>パスワード再設定メールの送信</h1>
      <div>
        <Form>
            <FormGroup>
              <Label>
                メールアドレス
              </Label>
              <Input
                type="email"
                name="email"
                style={{ height: 50, fontSize: "1.2rem" }}
                // ユーザーが入力したメールアドレスを取得する
                onChange={(e) => setEmail(e.target.value)}
              />
            </FormGroup>
            <Button
                style={{ width: 220 }}
                color="primary"
                // ボタンを押すとdoResetEmaiが実行される
                onClick={()=>{
                  sendResetEmail();
                }}
              >
              送信
            </Button>
        </Form>
      </div>
    </div>
  )
}

上記のコードにあるresetPasswordForEmailという関数がSupabaseで用意されているパスワード再設定のメールを送るための関数で、パスワード再設定画面のリンクが記載されたメールを指定のメールアドレスに送信することができます。

パスワード再設定画面のリンクは後ほど設定します。

続いてはパスワード再設定ページでユーザーが新しいパスワードを設定するための画面と処理を作成していきます。

Firebaseの場合はユーザーがパスワードを再設定するための画面もFirebaseの方で用意してくれているので、パスワードの再設定メールを送信する機能だけをつくればパスワードの再設定ができるようになっていました。

しかし、Supabaseではユーザーが新しいパスワードを入力して、パスワードを再設定するための画面と処理もこちらで用意する必要があります。

では、プロジェクトのpagesディレクトリにreset_password.jsというファイルを作成し、中身を以下のようにしてください。

pages/reset_password.js
import styles from '../styles/Home.module.css'
// 現時点で使わないものもあるが今後のことを考えて入れておく
import { Col, Container, Form, FormGroup, Input, Label, Row, Button } from "reactstrap";
import { useState } from 'react';

// supabase
import { supabase } from '../utils/supabase';
// useRouterをインポート
import { useRouter } from 'next/router';

export default function ResetPassword() {
  const [password, setPassword] = useState('');
  const router = useRouter();

  // パスワードを変更する処理
  const doResetPassword = async () => {
    // supabaseで用意されているユーザー情報を変更するための関数
    const { user, error } = await supabase.auth.updateUser(
        // ユーザーが入力したパスワードがsetPasswordでpasswordに格納される
        {
          password: password
        }
      )
    if (error) throw new Error(error.message)
    // ログインページに遷移
    router.push('/login')
  }

  return (
    <div className={styles.card}>
      <h1>新しいパスワードを入力してください</h1>
      <div>
        <Form>
            <FormGroup>
              <Label>
                パスワード
              </Label>
              <Input
                type="password"
                name="password"
                style={{ height: 50, fontSize: "1.2rem" }}
                // ユーザーが入力したメールアドレスを取得する
                onChange={(e) => setPassword(e.target.value)}
              />
            </FormGroup>
            <Button
                style={{ width: 220 }}
                color="primary"
                // ボタンを押すとdoResetEmaiが実行される
                onClick={()=>{
                  doResetPassword();
                }}
              >
              送信
            </Button>
        </Form>
      </div>
    </div>
  );
}

reset_password.jsが新しいパスワードを設定するための画面になるので、先ほどのforgot_password.jsにこちらの画面のリンクを設置します。

pages/forgot_password.js
    // supabaseで用意されているパスワード再設定のメールを送信するための関数
    const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
      // パスワード再設定画面のリンク
      redirectTo: 'http://localhost:3000/reset_password',
    })

次に_app.jsを開いて中身を以下のように編集します。

_app.js
import 'bootstrap/dist/css/bootstrap.min.css';
import { supabase } from '../utils/supabase'
import { useEffect } from 'react';

export default function App({ Component, pageProps }) {
  // パスワードを忘れた場合に再設定するための関数 これがないとパスワード再設定のときにエラーが起きる
  useEffect(() => {
    supabase.auth.onAuthStateChange((event, session) => {
      // パスワード再設定のときにログインしていない状態でもパスワードを変更できるようにするための処理
      if (event == 'PASSWORD_RECOVERY') {
        console.log('PASSWORD_RECOVERY', session)
        showPasswordResetScreen(true)
      }
    })
  },[])

  return <Component {...pageProps} />
}

ユーザーがパスワードを忘れてしまった場合のパスワード再設定に関しては、ログインしていない状態でも新しいパスワードを設定することができるように、_app.jsに以下のような処理を追加しています。

_app.js
  // パスワードを忘れた場合に再設定するための関数 これがないとパスワード再設定のときにエラーが起きる
  useEffect(() => {
    supabase.auth.onAuthStateChange((event, session) => {
      // パスワード再設定のときにログインしていない状態でもパスワードを変更できるようにするための処理
      if (event == 'PASSWORD_RECOVERY') {
        console.log('PASSWORD_RECOVERY', session)
        showPasswordResetScreen(true)
      }
    })
  },[])

パスワードを忘れてしまって再設定する場合、ユーザーはログインしていない状態なので、こちらの処理を書いておかないと新しいパスワードを登録する処理がエラーになってしまいます。

ここまでできたら、自分のメールアドレスにパスワード再設定メールを送信し、実際に新しいパスワードを設定するところまでやってみましょう。
スクリーンショット 2023-02-01 14.52.02.png

新しくパスワードを再設定して、そのパスワードでログインできることが確認できたら、パスワード再設定機能の実装は完了です。
スクリーンショット 2023-02-01 14.50.45.png

まとめ

Supabaseにも

・signUp
・signInWithPassword
・onAuthStateChange
・resetPasswordForEmail
・updateUser

などのログインやユーザー登録のための関数があらかじめ用意されているので、そういった関数を使うことで認証機能は比較的簡単に実装することができます。

今回の記事で解説したコードのGithubのリポジトリはこちらです。
https://github.com/masakiwakabayashi/nextjs_supabase_auth

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7