코딩/React

Supabase로 로그인 구현하기 with NextJS 5편

드리프트 2022. 1. 7. 18:56
728x170

 

안녕하세요?

 

Supabase로 로그인 구현하기 5편입니다.

 

일단 이전 편 못 보신 분들을 위해 전편 링크 걸어두겠습니다.

 

https://cpro95.tistory.com/617

 

Supabase로 로그인 구현하기 with NextJS 1편

안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱을 만들려고 노력하고 있는데요. 최근에는 NextAuth와 Prisma를 이용해서 카카오 로그인, 네이버 로그인, 구글 로그인 등 다방면으로 구현해 봤

cpro95.tistory.com

 

https://cpro95.tistory.com/618

 

Supabase로 로그인 구현하기 with NextJS 1편

안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱을 만들려고 노력하고 있는데요. 최근에는 NextAuth와 Prisma를 이용해서 카카오 로그인, 네이버 로그인, 구글 로그인 등 다방면으로 구현해 봤

cpro95.tistory.com

 

https://cpro95.tistory.com/619

 

Supabase로 로그인 구현하기 with NextJS 3편

안녕하세요? 지난 시간에 이어 Supabase 서비스를 이용한 NextJS 로그인 구현을 이어 나가도록 하겠습니다. 1, 2편 링크는 아래를 참조해 주시기 바랍니다. https://cpro95.tistory.com/617 Supabase로 로그인 구.

cpro95.tistory.com

 

https://cpro95.tistory.com/620

 

Supabase로 로그인 구현하기 with NextJS 4편

안녕하세요? Supabae 로그인 구현 4편입니다. 전편 링크는 아래와 같습니다. https://cpro95.tistory.com/617 Supabase로 로그인 구현하기 with NextJS 1편 안녕하세요? 최근 NextJS로 여러가지 로그인 구현 웹앱..

cpro95.tistory.com

 

이번 시간에는 유저가 로그인했을 경우를 체크하는 부분인데요.

 

/profile 라우팅으로 profile 페이지를 만들어서 유저가 로그인됐을 때 반응하는 페이지를 만들어 보겠습니다.

 

/pages/profile.tsx

import Link from "next/link";
import { useAuth } from "../lib/auth";

const ProfilePage = ({}) => {
  const { user, loading, signOut } = useAuth();

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div className="flex flex-col items-center justify-start py-36 min-h-screen">
      <h2 className="text-4xl my-4">
        Hello, {user && user.email ? user.email : "Supabase User!"}
      </h2>
      {!user && (
        <div>
          You have landed on a protected page.
          <br />
          Please{"  "}
          <Link href="/auth">
            <a className="font-bold text-blue-500">Log In</a>
          </Link>{" "}
          to view the page's full content.
        </div>
      )}
      {user && (
        <div>
          <button
            className="border bg-gray-500 border-gray-600 text-white px-3 py-2 rounded w-full text-center transition duration-150 shadow-lg"
            onClick={signOut}
          >
            Sign Out
          </button>
        </div>
      )}
    </div>
  );
};

export default ProfilePage;

 

유저가 로그인이 안 됐을 때 Log In 하라고 안내하는 프로파일(Profile) 페이지입니다.

 

코드를 보시면 useAuth() 에서 안 보던 게 있습니다.

 

바로 user, signOut인데요.

 

user는 로그인된 user 객체입니다.

 

supabase 문서에는 다음과 같이 나와 있습니다.

 

interface User {
    id: string;
    app_metadata: /* retracted */;
    user_metadata: {
        /* non-frequent user related values */
    };
    aud: string;
    confirmation_sent_at?: string;
    email?: string;
    created_at: string;
    confirmed_at?: string;
    last_sign_in_at?: string;
    role?: string;
    updated_at?: string;
}

 

우리가 쓸 User의 항목은 바로 email 이겠죠.

 

signOut은 로그아웃 함수인데요.

 

supabase의 로그아웃 함수는 간단합니다.

 

const signOut = async () => await supabase.auth.signOut();

 

바로 supabase.auth.signOut() 입니다.

 

그럼 바로 useAuth() 리액트 훅을 수정해 볼까요?

 

/lib/auth/AuthContext.tsx 파일을 수정할 예정입니다.

 

import { createContext, FunctionComponent, useState, useEffect } from "react";
import Router from "next/router";
import { User } from "@supabase/supabase-js";
import { supabase } from "../supabase";
import { useMessage, MessageProps } from "../message";
import { SupabaseAuthPayload } from "./auth.types";

export type AuthContextProps = {
  user: User;
  signUp: (payload: SupabaseAuthPayload) => void;
  signIn: (payload: SupabaseAuthPayload) => void;
  signOut: () => void;
  loading: boolean;
  loggedIn: boolean;
  userLoading: boolean;
};

 

AuthContextProps에 user와, signOut 함수, 그리고 loggedIn, userLoading 불린 값을 추가했습니다.

 

export const AuthProvider: FunctionComponent = ({ children }) => {
  const [loading, setLoading] = useState(false);
  
  const [user, setUser] = useState<User>(null);
  const [userLoading, setUserLoading] = useState(true);
  const [loggedIn, setLoggedIn] = useState(false);
  
  const { handleMessage } = useMessage();
  
  
  ....
  ....
  ....

그리고 AuthProvider에서도 리액트 State로써 user, userLoading, loggedIn을 useState로 추가했습니다.

 

그리고 signIn 함수에 handleMessage를 하나 추가했습니다.

// sign-in a user with provided details
  const signIn = async (payload: SupabaseAuthPayload) => {
    try {
      setLoading(true);
      const { error, user } = await supabase.auth.signIn(payload);
      if (error) {
        console.log(error);
        handleMessage({ message: error.message, type: "error" });
      } else {
        handleMessage({
          message: "Log in successful. I'll redirect you once I'm done",
          type: "success",
        });
        handleMessage({ message: `Welcome, ${user.email}`, type: "success" });
      }
    } catch (error) {
      console.log(error);
      handleMessage({
        message: error.error_description || error,
        type: "error",
      });
    } finally {
      setLoading(false);
    }
  };

supabase.auth.signIn 함수를 호출할 때 user 도 불러왔고, 그걸 이용해서 Welcome ${user.email} 방식으로 메시지 처리했습니다.

 

그리고 signIn 함수 밑에 signOut 함수를 아래와 같이 추가하시면 됩니다.

 

const signOut = async () => await supabase.auth.signOut();

 

마지막으로 중요한 부분이 있는데요.

 

바로 로그인한 유저의 액션에 따라서 페이지를 달리 보여줘야 하기 때문에 유저의 액션 상태를 항상 체크해야 하는 뭔가가 있어야 합니다.

 

supabase에서는 supabase.auth.onAuthStateChange(async(event, session) => {... }) 함수를 지원합니다.

 

event 파라미터는 'SIGNED_IN' | 'SIGNED_OUT' | 'USER_UPDATED' | 'PASSWORD_RECOVERY' 여기 중에 하나고요.

 

그러니까 유저가 로그인, 로그아웃, 유저 업데이트, 패스워드 리커버리가 됐을 때 onAuthStateChange 함수가 반응한다는 뜻입니다.

 

그래서 우리는 리액트의 useEffect 함수를 이용해서 맨 처음 실행되게 할 예정입니다.

 

AuthContext.tsx 파일의 signOut 함수 부분 아래에 아래의 useEffect 코드를 넣어주십시오.

 

const signOut = async () => await supabase.auth.signOut();

  useEffect(() => {
    const user = supabase.auth.user();

    if (user) {
      setUser(user);
      setUserLoading(false);
      setLoggedIn(true);
      Router.push("/profile");
    } else {
      setUserLoading(false);
    }

    const { data: authListener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        const user = session?.user! ?? null;
        setUserLoading(false);
        if (user) {
          setUser(user);
          setLoggedIn(true);
          Router.push("/profile");
        } else {
          setUser(null);
          setLoading(false);
          setLoggedIn(false);
          Router.push("/auth");
        }
      }
    );

    return () => {
      authListener.unsubscribe();
    };
  }, []);

 

supabase.auth.user() 함수로 user 객체를 얻을 수 있고요.

 

만약 user가 있다면 /profile 페이지로 라우팅 시킵니다.

 

그리고 supabase.auth.onAuthStateChange 함수를 이용할 건데요.

 

user 상태가 변경됐을 때 그에 맞게 setUser(null) 해주거나 setUser(user) 해주거나,

 

setLoggedIn을 true, false로 바꿔주고 또 그에 맞게 라우팅 해주는 방식입니다.

 

그리고, 페이지를 떠날 때 authListener.unsubscribe() 호출해주고 있습니다.

 

마지막으로 AuthContext.Provider의 value에 우리가 추가한 값을 넣어주면 됩니다.

 

return (
    <AuthContext.Provider
      value={{
        user,
        signUp,
        signIn,
        signOut,
        loading,
        loggedIn,
        userLoading,
      }}
    >
      {children}
    </AuthContext.Provider>
  );

 

테스트해 보니까 아주 잘 됩니다.

 

그리고 Navbar 부분도 바꿨는데요.

 

아래 코드입니다.

 

import React, { useState } from "react";
import { FaBars, FaBuffer, FaTimes } from "react-icons/fa";
import { useAuth } from "../lib/auth";

const Navbar = () => {
  const [menuToggle, setMenuToggle] = useState(false);
  const { user, loggedIn, signOut } = useAuth();

  let status = "not authenticated";
  return (
    //   navbar goes here
    <nav className="bg-gray-100">
      <div className="max-w-6xl mx-auto px-4">
        <div className="flex justify-between">
          <div className="flex space-x-4">
            {/* logo */}
            <div>
              <a href="/" className="flex items-center py-5 px-2 text-gray-700">
                <FaBuffer className="w-6 h-6" />
                <span className="font-bold px-2">Home</span>
              </a>
            </div>

            {/* primary nav */}
            <div className="hidden md:flex items-center space-x-1">
              <a
                href="/profile"
                className="py-5 px-3 text-gray-700 hover:text-gray-900"
              >
                Profile
              </a>
              <a
                href="#"
                className="py-5 px-3 text-gray-700 hover:text-gray-900"
              >
                Pricing
              </a>
            </div>
          </div>
          {/* secondary nav */}
          {loggedIn ? (
            <div className="hidden md:flex items-center space-x-1">
              ({user?.email})
              <button className="py-5 px-3" onClick={signOut}>
                Log out
              </button>
            </div>
          ) : (
            <div className="hidden md:flex items-center space-x-1">
              <a href="/auth" className="py-5 px-3">
                Login
              </a>
              <a
                href="/auth"
                className="py-2 px-3 bg-yellow-400 hover:bg-yellow-300 text-yellow-900 hover:text-yellow-800 rounded transition duration-300"
              >
                Signup
              </a>
            </div>
          )}
          {/* mobile menu */}
          <div className="md:hidden flex items-center">
            <button onClick={() => setMenuToggle(!menuToggle)}>
              {menuToggle ? (
                <FaTimes className="w-6 h-6" />
              ) : (
                <FaBars className="w-6 h-6" />
              )}
            </button>
          </div>
        </div>
      </div>

      {/* mobile menu items */}
      <div className={`${!menuToggle ? "hidden" : ""} md:hidden`}>
        <a
          href="/profile"
          className="block py-2 px-4 text-sm hover:bg-gray-200"
        >
          Profile
        </a>
        <a href="#" className="block py-2 px-4 text-sm hover:bg-gray-200">
          Pricing
        </a>

        {loggedIn ? (
          <button
            className="block py-2 px-4 text-sm hover:bg-gray-200"
            onClick={signOut}
          >
            Log out
          </button>
        ) : (
          <div>
            <a
              href="/auth"
              className="block py-2 px-4 text-sm hover:bg-gray-200"
            >
              Login
            </a>
            <a
              href="/auth"
              className="block py-2 px-4 text-sm hover:bg-gray-200"
            >
              Signup
            </a>
          </div>
        )}
      </div>
    </nav>
  );
};

export default Navbar;

 

이상으로 5편을 마치겠습니다.

 

그리드형