💡

半年間の長期インターンを始めたので、1ヶ月ごとに学んだこととかを吐き出してみるの会【12月編】

2025/01/15に公開

シリーズまとめ

https://zenn.dev/seiwell/articles/ee64863ad2ab88

はじめに

あけましておめでとうございます。本年もよろしくお願いします!

今年は、幸いなことに昨年とは違い、平穏な新年を迎えることができましたね。
なんの縁か分かりませんが、隣国では悲しいことに飛行機事故がまたしても起きてしまい、来年こそは何事もなく、新年を迎えたいですね。

さて、今月はState管理、処理のイメージに頭を悩ませていました。
そもそも、Stateのイメージがまだ完全ではなく、なんとなく使っている(使ってしまっている)&漠然としてしまっている状態でした。そのため、発展的な使い方やコンポーザブル上でのグローバルなState管理を学ぶ必要があるなと思い、年末年始もあったのでStateの勉強がてら個人の方で取り組んだFirebase Authを使った認証実装について、書き起こしていこうと思います。

前提要件として、ログイン状態の永続化が必要であったことから、Nuxt標準のuseState()ではなく、Storeを使いました。

Storeとは?

簡単に言えば、Stateの拡張版です。アプリケーションの状態を保持・変更することができます。Stateはブラウザやタブを閉じると、状態が初期化されてしまいますが、Storeは永続化すれば、閉じても保持してくれます。保存先はローカルストレージ or セッションストレージに自動的に格納されます。(なお、デフォでローカルストレージのようでした。)

Storeの特徴

  • Stateの永続化が可能(ブラウザやタブを閉じてもStateの復元ができる)
  • DevTools上で、状態の変更の追跡・デバッグが手軽
  • 変更前、変更後の値(状態)の取得ができる
    など、useState()より拡張され、より開発しやすく、使いやすくなっています。

Storeを触ってみる

Nuxt2まではStoreはあったようなのですが、3から廃止されてしまっているため、今回はみんな大好きPiniaを使います。

pnpm install pinia pinia-plugin-persistedstate firebase

個人的には、pnpmが好きで使っているのでpnpmになっていますが、環境によって適宜変更してください。

piniaとfirebaseのインストールをしながら、アプリディレクトリ上にstoresという名前のディレクトリを作成してください。
現在のディレクトリ構造

/
├─ assets
├─ components
├─ composables
├─ layouts
├─ middleware
├─ pages
├─ plugins
├─ public
├─ server
└─ stores (※このフォルダを作成)

今作ったstoresにコードを書くことで、状態管理をすることができます。

さっそく実装・・・の前に

Firebaseの設定を先に済ませておきましょう。
https://console.firebase.google.com/?hl=ja
にアクセスします。

「プロジェクトを作成する」から、プロジェクト名を適当に決めます。

この辺は、適宜カスタマイズしてください。

アナリティクスを有効にした場合、下記の画面になると思います。デフォルトのアカウントを選択しておけば問題ありません。Firebaseが良しなにやってくれます。

問題なさそうであれば、「プロジェクトを作成」を押して、しばらく待ちましょう。

作成が完了すると、こんな感じの画面になると思うので、「続行」します。

トップページが表示されたら、サイドバーの構築から、Authenticationを探します。

「始める」を押して、

ログインプロバイダを選択します。今回は、メール/パスワードとGoogleを選択します。

ログインプロバイダの追加が完了したら、プロジェクトの設定画面を開きます。
Androidアイコンの隣にあるWebアプリを選択します。

適当に名前を入れます。今回は、Firebase Hostingは使用しないので、チェックしなくて問題はないです。

すると、APIキーやら色々と値が出てくるので、控えておいてください。後ほど使います。

これで、Firebaseの設定は完了です。

今度こそ、実装

余談ですが、Nuxt3でFirebase Authを実装している記事(最新)のがなく、割と大変だったので、参考になれば幸いです。

Store

まずは、stores配下にコードを追加します。今回は、useAuth.tsというファイルを作成しました。
VS CodeでNuxtrという拡張機能を入れている方の場合、下記のような雛形ファイルが自動的に作成されるかと思います。(最終的なコードは、本記事の末尾に記載しています)

export const useAuthStore = defineStore('auth', () => {
    // ここに具体的な処理を書いていく
}

まずは、必要なライブラリ類をインポートしていきます。

import { defineStore } from 'pinia'
import {
  createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, User
} from 'firebase/auth'
import { ref } from 'vue'

今回は、メールアドレス&パスワードを使った認証とGoogleを使った認証の2つを実装していきますので、事前に型定義をしてあげます。AnyだとTypeScript君、拗ねちゃうので()

interface AuthValues {
  email: string
  password: string
}

そうしたら、メインディッシュとなるコードを書いていきます。
必要な関数として、
 ・ユーザーを作成する関数(メールアドレス&パスワード認証)
 ・メールアドレス&パスワードでログインする関数
 ・Googleを使ってログインする関数
 ・ログアウトをする関数
の4つの関数を、とりあえず書いていきます。
詳しいコードの中身については、コメントアウトに記載したので参照してください。

// 認証関連の状態管理ストアを定義
export const useAuthStore = defineStore('auth', () => {
  // 現在ログインしているユーザーの情報を保持するリアクティブな状態
  // null の場合は未ログイン状態を表す
  const user = ref<User | null>(null)

  /**
   * 新規ユーザーをメール/パスワードで登録する関数
   * @param values - メールアドレスとパスワードを含むオブジェクト
   */
  async function createUser(values: AuthValues) {
    const auth = getAuth()
    const userCredential = await createUserWithEmailAndPassword(auth, values.email, values.password)
    user.value = userCredential.user
  }

  /**
   * 既存ユーザーをメール/パスワードでログインさせる関数
   * @param values - メールアドレスとパスワードを含むオブジェクト
   */
  async function signInUser(values: AuthValues) {
    const auth = getAuth()
    const userCredential = await signInWithEmailAndPassword(auth, values.email, values.password)
    user.value = userCredential.user
  }

  /**
   * Googleアカウントを使用してユーザーをログインさせる関数
   * ログイン後、ユーザー情報をAmplifyデータベースと同期する
   */
  async function signInUserWithGoogle() {
    // Googleログインのポップアップを表示
    const auth = getAuth()
    const userCredential = await signInWithPopup(auth, new GoogleAuthProvider)
    user.value = userCredential.user
  }

  /**
   * ユーザーをログアウトさせる関数
   * ログアウト後、ローカルのユーザー状態をクリアする
   */
  async function signOutUser() {
    const auth = getAuth()
    await signOut(auth)
    user.value = null
  }

  // ストアから外部に公開する状態と関数
  return {
    user,             // 現在のユーザー状態
    createUser,       // 新規ユーザー登録
    signInUser,       // メール/パスワードログイン
    signInUserWithGoogle,  // Googleログイン
    signOutUser,      // ログアウト
  }
}

最後に、ブラウザやタブを閉じてもStateを保持されるようにします。

()
    signOutUser,      // ログアウト
  }
},
{
  // ページのリロード後もユーザーのログイン状態を維持する設定
  // localStorageにストアの状態が保存される
  // 逆にこれがないとログイン状態を保持してくれない
  persist: true
}

なんと意外にも、これでStoreの実装は終わりです。あら簡単。

最終的なコード全体
import { defineStore } from 'pinia'
import {
  createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, User
} from 'firebase/auth'
import { ref } from 'vue'

interface AuthValues {
  email: string
  password: string
}

export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)

  async function createUser(values: AuthValues) {
    const auth = getAuth()
    const userCredential = await createUserWithEmailAndPassword(auth, values.email, values.password)
    user.value = userCredential.user
  }

  async function signInUser(values: AuthValues) {
    const auth = getAuth()
    const userCredential = await signInWithEmailAndPassword(auth, values.email, values.password)
    user.value = userCredential.user
  }

  async function signInUserWithGoogle() {
    const auth = getAuth()
    const userCredential = await signInWithPopup(auth, new GoogleAuthProvider)
    user.value = userCredential.user
  }

  async function signOutUser() {
    const auth = getAuth()
    await signOut(auth)
    user.value = null
  }

  return {
    user,
    createUser,
    signInUser,
    signInUserWithGoogle,
    signOutUser,
  }
},
{
  persist: true
}
)

plugins(firebaseの設定)・NuxtConfig

Nuxt3では、plugins配下にあるコードは、特に何もせずに自動インポートがされるので、こちらにFirebaseの初期化処理・認証処理を記載していきます。
ファイル名は、firebase.client.tsです。

import { initializeApp } from 'firebase/app'
import { getAuth, onAuthStateChanged, type Auth, type User } from 'firebase/auth'
import type { RuntimeConfig } from '@nuxt/schema'
import { useAuthStore } from '~/stores/useAuth';

export default defineNuxtPlugin((nuxtApp) => {
  const config: RuntimeConfig = useRuntimeConfig()
  const firebaseConfig = {
    apiKey: config.public.firebaseApiKey,
    authDomain: config.public.firebaseAuthDomain,
    projectId: config.public.firebaseProjectId,
    storageBucket: config.public.firebaseStorageBucket,
    messagingSenderId: config.public.firebaseMessagingSenderId,
    appId: config.public.firebaseAppId,
    measurementId: config.public.firebaseMeasurementId
  }
  const app = initializeApp(firebaseConfig)
  const auth: Auth = getAuth(app)
  const authStore = useAuthStore()

  onAuthStateChanged(auth, (user: User | null) => {
    if (user) {
      authStore.user = user
    }
    else {
      authStore.user = null
    }
  })

})

NuxtConfig

次に、FirebaseのAPIキーなどを環境変数に格納するために、nuxt.config.tsに追記していきます。

  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL,
      firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
      firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
      firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID,
      firebaseStorageBucket: process.env.NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
      firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
      firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID,
      firebaseMeasurementId: process.env.NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID
    }
  },

追記ができたら、nuxt.config.tsと同じ階層上に.envを作成し、先ほど控えたFirebaseの値を格納しておきます。

NUXT_PUBLIC_FIREBASE_API_KEY=
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NUXT_PUBLIC_FIREBASE_PROJECT_ID=
NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NUXT_PUBLIC_FIREBASE_APP_ID=
NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID=

ログインページの実装

デザインはかなりシンプルにしているので、自分でいい感じにデザインしてください。
長くなってしまうので、トグル内に収めてます↓

フロントデザイン部分
<template>
  <div class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
    <div class="max-w-md w-full space-y-8 bg-white p-8 rounded-lg shadow-sm">
      <div>
        <h2 class="mt-6 text-center text-3xl font-bold text-gray-900">
          ログイン
        </h2>
      </div>
      
      <form @submit.prevent="handleEmailAndPasswordLogin" class="mt-8 space-y-6">
        <div class="rounded-md -space-y-px">
          <div>
            <label for="email" class="sr-only">メールアドレス</label>
            <input 
              id="email"
              type="email"
              v-model="form.email"
              required
              class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
              placeholder="メールアドレス"
            />
          </div>
          <div>
            <label for="password" class="sr-only">パスワード</label>
            <input
              id="password"
              type="password" 
              v-model="form.password"
              required
              class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
              placeholder="パスワード"
            />
          </div>
        </div>

        <div>
          <button 
            type="submit"
            class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
            ログイン
          </button>
        </div>
      </form>

      <div class="relative">
        <div class="absolute inset-0 flex items-center">
          <div class="w-full border-t border-gray-300"></div>
        </div>
        <div class="relative flex justify-center text-sm">
          <span class="px-2 bg-white text-gray-500">または</span>
        </div>
      </div>

      <div>
        <button
          @click="handleWithGoogleLogin"
          class="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          <svg class="h-5 w-5 mr-2" viewBox="0 0 48 48">
            <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
            <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
            <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
            <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
          </svg>
          Googleでログイン
        </button>
      </div>
    </div>
  </div>
</template>

次に内部実装部分

<script lang="ts" setup>
import { ref } from 'vue'

import { useAuthStore } from '~/stores/useAuth';

const auth = useAuthStore()

const form = reactive({
    email: '',
    password: '',
})

const handleEmailAndPasswordLogin = async() => {
  await auth.signInUser(form)
}

const handleWithGoogleLogin = async() => {
  await auth.signInUserWithGoogle()
}
</script>

これで、Firebaseの設定が完了していれば、問題なくログインができるようになっているはずです。

比較的簡単...!

簡単とは書いていますが、1週間くらい頭を悩まされたくらいには大変だったので、NuxtでFirebase Authを実装したい方の参考になれば幸いです。

最後に

個人事業がかなり順調に話が進んでいるので、2025年は個人事業にも力を入れつつ、大学生活も残り3年なので、駆け抜けて参りたいと思います。インフルエンザとコロナが流行っているので、どうぞご自愛ください。また次回。

リバナレテックブログ

Discussion