안녕하세요?
Supabase로 로그인 구현하기 5편입니다.
일단 이전 편 못 보신 분들을 위해 전편 링크 걸어두겠습니다.
https://cpro95.tistory.com/617
https://cpro95.tistory.com/618
https://cpro95.tistory.com/619
https://cpro95.tistory.com/620
이번 시간에는 유저가 로그인했을 경우를 체크하는 부분인데요.
/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편을 마치겠습니다.
'코딩 > React' 카테고리의 다른 글
Supabase로 로그인 구현하기 with NextJS 7편 (0) | 2022.01.09 |
---|---|
Supabase로 로그인 구현하기 with NextJS 6편 (0) | 2022.01.07 |
Supabase로 로그인 구현하기 with NextJS 4편 (0) | 2022.01.07 |
Supabase로 로그인 구현하기 with NextJS 3편 (1) | 2022.01.06 |
Supabase로 로그인 구현하기 with NextJS 2편 (0) | 2022.01.05 |