React(Next.js) × Firebase에서 전체 개발 인증 주위

개요


제목이 그대로 유지됨(Next.js)× Firebase 환경에서 인증 주위 전체가 개발됐기 때문에 기사에 남는다.
경고와 같은 구성 요소는 Material UI를 사용합니다.

해본 일


다음 네 개.
  • 사용자 등록
  • 로그인
  • 로그오프
  • 액세스 제어
  • 로그인하지 않았을 때 특정 페이지에 들어가려면 경보 & 이동
  • 로그인 시 로그인 페이지 등에 가면 경보 & 이전
  • 전제 조건

  • react: 17.0.2
  • next: 12.0.9
  • typescript: 4.5.5
  • firebase: 9.6.5
  • emotion: 11.0.0
  • 그리고 미리 Firebase.ts라는 곳에서 다음과 같은 설정을 했습니다.
    firebase.ts
    import firebase from "firebase/compat/app"
    import "firebase/compat/auth"
    import "firebase/compat/firestore"
    import "firebase/compat/storage"
    
    const firebaseConfig = {
      apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
      authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
      messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGE_SENDER_ID,
      appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
    }
    
    const firebaseApp = firebase.initializeApp(firebaseConfig)
    
    export const auth = firebase.auth()
    export default firebase
    

    사용자 등록


    일단 자세부터 잡자.야!
    Signup.tsx
    <form onSubmit={handleSubmit}>
      <div
        css={css`
          display: flex;
          justify-content: center;
          align-items: center;
        `}
      >
        <InputLabel>メールアドレス</InputLabel>
        <TextField
          name="email"
          type="email"
          size="small"
          onChange={handleChangeEmail}
          css={css`
            padding-left: 12px;
          `}
        />
      </div>
      <div
        css={css`
          display: flex;
          justify-content: flex-end;
          align-items: center;
          margin-top: 16px;
        `}
      >
        <InputLabel>パスワード</InputLabel>
        <TextField
          name="password"
          type="password"
          size="small"
          onChange={handleChangePassword}
          css={css`
            padding-left: 12px;
          `}
        />
      </div>
      <div
        css={css`
          display: flex;
          justify-content: flex-end;
          margin-top: 16px;
        `}
      >
        <Button type="submit" variant="outlined">
          登録
        </Button>
      </div>
      <div
        css={css`
          display: flex;
          justify-content: flex-end;
          margin-top: 24px;
        `}
      >
        <Link href={"/login"}>
          <a>すでに登録している人はこちら</a>
        </Link>
      </div>
    </form>
    
    InputLabel, Text Field, Button은 Material UI, Link는 Next입니다.
    잘 부탁드립니다.
    CSS도 잘 어울려요.
    handleSubmit, handleChangePassword는 정의되지 않았습니다.용도는 이름과 같이 각각 설치해 주십시오.
    Signup.tsx
    import { auth } from "../src/firebase"
    
    const router = useRouter()
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    
    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault()
      await auth.createUserWithEmailAndPassword(email, password)
      router.push("/")
    }
    const handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
      setEmail(e.currentTarget.value)
    }
    const handleChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
      setPassword(e.currentTarget.value)
    }
    
    handleChangeEmail,handleChangePassword는 state의 set만 합니다.문자를 입력할 때마다 해당하는 state에 값을 저장합니다.
    그리고 저장된 이메일,password를Firebase로 나누어 새 사용자를 등록합니다.
    그걸 진행한 건handle Submit 내auth.createUserWithEmailAndPassword(email, password)야.
    이 줄은 사용자를 등록할 수 있어 Firebase가 강하다.
    사용자가 등록한 후 루트 페이지로 이동합니다.노선으로 날아갔지만 여긴 당연히 어디든 가능하지.
    이상은 새로 등록한 일입니다.
    코드 전문 여기 있습니다.
    Signup.tsx
    Signup.tsx
    import React, { useState } from "react"
    import { useRouter } from "next/router"
    import Link from "next/link"
    import { Alert, Button, InputLabel, Snackbar, TextField } from "@mui/material"
    import { css } from "@emotion/react"
    
    import { auth } from "../src/firebase"
    import { useAuthContext } from "../src/context/AuthContext"
    
    const Signup = () => {
      const router = useRouter()
      const { user } = useAuthContext()
      const isLoggedIn = !!user
      const [email, setEmail] = useState("")
      const [password, setPassword] = useState("")
    
      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        await auth.createUserWithEmailAndPassword(email, password)
        router.push("/")
      }
      const handleClose = async () => {
        await router.push("/")
      }
      const handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(e.currentTarget.value)
      }
      const handleChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
        setPassword(e.currentTarget.value)
      }
      return (
        <div
          css={css`
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-flow: column;
          `}
        >
          <Snackbar
            open={isLoggedIn}
            anchorOrigin={{ vertical: "top", horizontal: "center" }}
            autoHideDuration={3000}
            key={"top" + "center"}
            onClose={handleClose}
          >
            <Alert onClose={handleClose} severity="warning">
              すでにログインしています
            </Alert>
          </Snackbar>
          <h2>ユーザー登録</h2>
          <form onSubmit={handleSubmit}>
            <div
              css={css`
                display: flex;
                justify-content: center;
                align-items: center;
              `}
            >
              <InputLabel>メールアドレス</InputLabel>
              <TextField
                name="email"
                type="email"
                size="small"
                onChange={handleChangeEmail}
                css={css`
                  padding-left: 12px;
                `}
              />
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                align-items: center;
                margin-top: 16px;
              `}
            >
              <InputLabel>パスワード</InputLabel>
              <TextField
                name="password"
                type="password"
                size="small"
                onChange={handleChangePassword}
                css={css`
                  padding-left: 12px;
                `}
              />
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                margin-top: 16px;
              `}
            >
              <Button type="submit" variant="outlined">
                登録
              </Button>
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                margin-top: 24px;
              `}
            >
              <Link href={"/login"}>
                <a>すでに登録している人はこちら</a>
              </Link>
            </div>
          </form>
        </div>
      )
    }
    
    export default Signup
    
    
    아래 액세스 제어에서 한 내용도 살짝 섞였으니 스냅바와 useAuthContext() 등 위에 설명되지 않은 내용은 잠시 무시해 주십시오.
    또한 완성형 UI는 여기에 있습니다.

    아니에요.다음에 갑시다!

    로그인


    논리는 기본적으로 사용자 로그인과 같다.
    실제 로그인 처리는 사용자 로그인과 같은 줄만 있습니다.
    특별히 쓸 일이 없기 때문에 코드 전문을 즉시 게재합니다.
    Login.tsx
    Login.tsx
    import React, { useState } from "react"
    import { useRouter } from "next/router"
    import Link from "next/link"
    import { Alert, Button, InputLabel, Snackbar, TextField } from "@mui/material"
    import { css } from "@emotion/react"
    
    import { auth } from "../src/firebase"
    import { useAuthContext } from "../src/context/AuthContext"
    
    const Login = () => {
      const { user } = useAuthContext()
      const isLoggedIn = !!user
      const router = useRouter()
      const [email, setEmail] = useState("")
      const [password, setPassword] = useState("")
      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        await auth.signInWithEmailAndPassword(email, password)
        router.push("/")
      }
      const handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(e.currentTarget.value)
      }
      const handleChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
        setPassword(e.currentTarget.value)
      }
      const handleClose = async () => {
        await router.push("/")
      }
    
      return (
        <div
          css={css`
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-flow: column;
          `}
        >
          <Snackbar
            open={isLoggedIn}
            anchorOrigin={{ vertical: "top", horizontal: "center" }}
            autoHideDuration={3000}
            key={"top" + "center"}
            onClose={handleClose}
          >
            <Alert onClose={handleClose} severity="warning">
              すでにログインしています
            </Alert>
          </Snackbar>
          <Snackbar
            open={!isLoggedIn}
            anchorOrigin={{ vertical: "top", horizontal: "center" }}
            autoHideDuration={3000}
            key={"top" + "center"}
          >
            <Alert severity="warning">ログインしてください</Alert>
          </Snackbar>
          <h2>ログイン</h2>
          <form onSubmit={handleSubmit}>
            <div
              css={css`
                display: flex;
                justify-content: center;
                align-items: center;
              `}
            >
              <InputLabel>メールアドレス</InputLabel>
              <TextField
                name="email"
                type="email"
                size="small"
                onChange={handleChangeEmail}
                css={css`
                  padding-left: 12px;
                `}
              />
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                align-items: center;
                margin-top: 16px;
              `}
            >
              <InputLabel>パスワード</InputLabel>
              <TextField
                name="password"
                type="password"
                size="small"
                onChange={handleChangePassword}
                css={css`
                  padding-left: 12px;
                `}
              />
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                margin-top: 16px;
              `}
            >
              <Button type="submit" variant="outlined">
                ログイン
              </Button>
            </div>
            <div
              css={css`
                display: flex;
                justify-content: flex-end;
                margin-top: 24px;
              `}
            >
              ユーザ登録は
              <Link href={"/signup"}>
                <a>こちら</a>
              </Link>
              から
            </div>
          </form>
        </div>
      )
    }
    
    export default Login
    
    
    await auth.signInWithEmailAndPassword(email, password)의 한 줄에 로그인 처리를 합니다.방법명도 알기 쉽고.
    이는 사용자 로그인과 마찬가지로 액세스 제어 내용이 뒤섞여 있어 내용을 대충 봐도 문제없다.
    우선 완성형 UI는 이쪽에 있습니다.

    취소


    다음handle Logiput, 로그아웃 버튼(상)에서 훈련합니다!
    import { auth } from "../firebase"
    import { useRouter } from "next/router"
    
    const router = useRouter()
    const handleLogout = async () => {
      await auth.signOut()
      await router.push("/about")
    }
    
    제 경우는 이 기사에 등장하지 않은 햄버거 메뉴의 버튼에 담겨 있습니다.
    먼저 해야 할 일은 위의 함수를 온클릭에 넣는 것이기 때문에 이곳의 전문 UI는 생략되었다.
    그래도 대충 인증 처리 한 줄이면 끝나요. 정말 간단하고 대단하네요.

    액세스 제어


    로그인하지 않은 경우


    로그인하지 않은 경우 특정 페이지에 들어가려면 로그인 페이지로 강제 마이그레이션한 후 "로그인하지 않았기 때문에 안 됩니다~"라고 경고합니다.
    특정한 것이라고는 하지만 거의 모든 페이지가 이 처리를 하려고 한다.
    따라서 모든 페이지 component 간섭 가능app.tsx에 뭐라도 더 써.
    추기 후의app.tsx의 코드 전문은 이런 느낌입니다.
    _app.tsx
    import { css } from "@emotion/react"
    import Header from "../src/components/header"
    import type { AppProps } from "next/app"
    import { AuthProvider } from "../src/context/AuthContext"
    
    const App = ({ Component, pageProps }: AppProps) => {
      return (
        <AuthProvider>
          <Header />
          <Component
            {...pageProps}
          />
        </AuthProvider>
      )
    }
    
    export default App
    
    헤더는 단지 꼬리표일 뿐, 햄버거 메뉴에만 나타나 Auth Provider가 어떤 녀석인지 느낀다.
    AuthProvider는 대체로 현재 로그인 여부를 감시하는 구성 요소입니다.
    감시에 의하면 땡땡이를 치지 않으면××이런 처리는 이 점에 연결될 수 있다.
    로그인 감시는auth입니다.onAuthStateChanged라는 방법으로 구현할 수 있습니다.
    이 방법은 예를 들면 다음과 같다.
    auth.onAuthStateChanged((user) => {
      if (user) {
        // ログイン時の処理
      } else {
        // 未ログイン時の処理
      }
    });
    
    로그인한 경우user에 사용자 정보가 있고 로그인하지 않으면null이 있습니다.
    이 방법을 사용하면 각각 로그인할 때와 미로그인할 때의 처리를 실현할 수 있다.
    이거 auth.AuthProvider에서 onAuthStateChanged를 사용합니다.
    이번 요구사항은'로그인하지 않을 때 특정 페이지로 들어가 강제로 로그인 페이지로 이동한 후 경보를 보내기'이기 때문에 어떤 페이지에 들어갈 때마다'로그인 여부','특정 페이지에 있는지 확인해야 한다.
    이런 건 useEffect로 이루어져요.
    따라서 AuthContext는 다음과 같이 구현됩니다.
    AuthContext.tsx
    AuthContext.tsx
    import { ReactNode, createContext, useState, useContext, useEffect } from "react"
    import firebase, { auth } from "../firebase"
    import { useRouter } from "next/router"
    
    type UserType = firebase.User | null
    
    type AuthContextProps = {
      user: UserType
    }
    
    type AuthProps = {
      children: ReactNode
    }
    
    const AuthContext = createContext<Partial<AuthContextProps>>({})
    
    export const useAuthContext = () => {
      return useContext(AuthContext)
    }
    
    export const AuthProvider = ({ children }: AuthProps) => {
      const router = useRouter()
      const [user, setUser] = useState<UserType>(null)
      const isAvailableForViewing =
        router.pathname === "/about" ||
        router.pathname === "/login" ||
        router.pathname === "/signup"
      const value = {
        user,
      }
    
      useEffect(() => {
        const authStateChanged = auth.onAuthStateChanged(async (user) => {
          setUser(user)
          !user && !isAvailableForViewing && (await router.push("/login"))
        })
        return () => {
          authStateChanged()
        }
      }, [])
    
      return (
        <AuthContext.Provider value={value}>
          {children}
        </AuthContext.Provider>
      )
    }
    
    
    !user && !isAvailableForViewing && (await router.push("/login"))에서 "지금 로그인하지 않았습니다. 열람 가능한 페이지에 들어가면 로그인 페이지로 이동합니다.
    또한 '현재 사용자가 로그인하는지 여부' 의 값도 다른 구성 요소를 가로질러 사용하기 때문에useContext로 실행됩니다.
    이렇게 하면'로그인하지 않았을 때 특정한 페이지로 들어가 강제로 로그인 페이지로 이동한 후 경보를 보낸다'의 절반이 완성된다.
    그리고 로그인 페이지에 경보를 보내세요.
    Login.tsx
    const Login = () => {
      const { user } = useAuthContext()
      const isLoggedIn = !!user
     
     return (
       ...(中略)...
       <Snackbar
         open={!isLoggedIn}
         anchorOrigin={{ vertical: "top", horizontal: "center" }}
         autoHideDuration={3000}
         key={"top" + "center"}
       >
        <Alert severity="warning">ログインしてください</Alert>
       </Snackbar>
      ...(中略)...
      )
    }
    
    게재됐으니 필요한 곳만 뽑아 로긴.tsx를 다시 불러옵니다.
    useAuthContext().사용자 정보가 들어오는 속성에 로그인하면user가 위에서 로컬 전체에 접근할 수 있습니다.
    bool로 변환한 후 가짜라면 로그인하지 않기 때문에 경보를 표시하는 Snakbar.
    Material UI의 Alert를 Snapbar에 배치한 이유는 Alert에 표시되는 proops가 없기 때문입니다.나는 공식을 모방하여 썼다.
    https://mui.com/components/snackbars/
    이렇게 하면 실현할 수 있다.
    경고의 UI는 이런 느낌입니다.

    로그인 시


    로그인 페이지와 새 로그인 페이지에 가면 "이미 로그인했어~"라는 경고가 울려 강제 마이그레이션됩니다.
    이쪽 처리도 이미 Logiin입니다.tsx, Signup.tsx 전문에 기재되어 있지만 필요한 부분만 다시 한 번 뜯어보겠습니다.
    Signup.tsx
    const Login = () => {
      const { user } = useAuthContext()
      const isLoggedIn = !!user
      
      const handleClose = async () => {
        await router.push("/")
      }
     
     return (
       ...(中略)...
        <Snackbar
          open={isLoggedIn}
          anchorOrigin={{ vertical: "top", horizontal: "center" }}
          autoHideDuration={3000}
          key={"top" + "center"}
          onClose={handleClose}
        >
          <Alert onClose={handleClose} severity="warning">
            すでにログインしています
          </Alert>
        </Snackbar>
      ...(中略)...
      )
    }
    
    Signup.tsx의 일부분을 싣고 있지만, Logiin.tsx에도 같은 일이 적혀 있다.
    로그인하지 않았을 때의 접근 제어가 닿은 것처럼useAuthContext ().user는 "로그인하면 사용자 정보가 있는 속성"이고 isLoggedIn이 바로 그 책입니다.
    따라서 로그인 상태에서 새 로그인 페이지, 로그인 페이지로 이동하면 경고가 먼저 표시됩니다.
    그리고 onClose에서handleClose에 불을 붙여 노선의 페이지로 강제로 이동합니다.
    이렇게 접근 제어도 완성되었습니다!🙏
    먼저 로그인할 때 경고 UI가 먼저 업로드됩니다.

    감상


    개발자를 위한 다양한 서비스와 결합해 새 앱을 만들 때'Firebase는 Auth만 사용한다'는 점을 간혹 발견할 수 있다.
    나는 그 이유를 조금 알 것 같다.Firebase Authentication, 간단하면서도 세게~

    참고 문장


    https://reffect.co.jp/react/react-firebase-auth#React_Router

    좋은 웹페이지 즐겨찾기