supabase를 통하여 로그인과 회원가입 기능을 구현했다. 그런데 막상 구현하고 나니 또 뭘 해야할지 모르겠는거임..!
로그인을 했으니까 마이페이지가 생기고 이동할 수 있어야 하고 그럴려면 로그인한 유저의 상태가 유지되고 데이터도 브라우저가 갖고 있어야겠고 그러기 위해서 유저데이터를 필요할 때마다 필요한 곳에 불러와야겠는거임.
그래서 지금 하고 있는 사이드프로젝트의 규모가 크지 않으니 context api를 사용할까 했음.
그런데 그렇게 되면 원하지 않은 곳에서 불필요한 리렌더링 일어날 것 같아 zustand를 사용하기로 함!
zustand는 필요할 때에 원하는 데이터만 바로 불러와 쓸 수 있는 간편함이 좋았고,
provider를 쓰지 않아 상태 변경 시 불필요한 리렌더링을 제어할 수 있기 때문임!
선행 작업으로 supabase의 데이터 테이블를 자동으로 타입 지정해주는 라이브러리를 사용했음.
타입 자동완성하는 방법은 아래 블로그 참고하면 된다.
이제 정말 zustand를 사용해보자!
1. zustand 설치하기
npm install zustand
or
yarn add zustand
위에 명령어를 입력하여 앱에 설치한다.
2. zustand store 만들기
// src/store/userStore.ts
import { create } from 'zustand';
import { User } from '@supabase/supabase-js';
interface UserState {
user: User | null;
setUser: (user: User | null) => void;
clearUser: () => void;
}
export const useUserStore = create<UserState>((set) => ({
// 'user'를 기본값 'null'로 초기화하여 처음에는 사용자가 로그인되어 있지 않음을 나타냄
user: null,
// 'setUser' 함수는 User 객체나 'null'을 받아 'set' 함수를 호출하여 'user' 상태를 업데이트함
setUser: (user) => set({ user }),
// 'clearUser' 함수는 'set'을 호출하여 'user' 상태를 'null'로 재설정함
clearUser: () => set({ user: null }),
}));
유저 정보를 저장할 store를 만들고, 타입스크립트 인터페이스로 유저 데이터를 정의한다.
3. 로그인 시 zustand의 setUser에 유저데이터 저장하기
const loginHandler = async (e: React.FormEvent) => {
e.preventDefault();
//유효성 검사
if (emailRef.current?.value === "") {
alert("아이디를 입력해주세요.");
return;
}
if (passwordRef.current?.value === "") {
alert("비밀번호를 입력해주세요.");
return;
}
//로그인 데이터 post
const { data, error } = await supabase.auth.signInWithPassword({
email: emailRef.current?.value || "",
password: passwordRef.current?.value || "",
});
if (error) throw error;
//로그인 한 유저의 data를 setUser에 넣기
const { setUser } = useUserStore.getState();
setUser(data.user);
alert("로그인 성공!");
navigate("/");
};
getState() 메서드는 zustand에서 제공하는 함수로, 현재 상태를 즉시 가져오는 역할을 한다고 함. 상태 변화에 대한 리렌더링을 유발하지 않아서 전역상태를 직접 변경하거나 사용할 때 편리함
4. 로그인 상태 유지를 위한 initializeUser 함수 만들기
// src/utils/initAuth.ts
import { useUserStore } from '../store/userStore';
import supabase from '../supabase/supabaseClient';
export const initializeUser = async () => {
try {
const { data, error } = await supabase.auth.getUser();
if (error) {
console.error("Failed to fetch user:", error);
return;
}
const { setUser } = useUserStore.getState();
if (data?.user) {
setUser(data.user);
}
} catch (err) {
console.error("Unexpected error in initializeUser:", err);
}
};
페이지 새로고침이나 앱 재시작 시 로그인 상태를 유지하기 위해 initializeUser 함수를 만든다.
supabase는 세션을 로컬스토리지에 저장한다. 그래서 새로고침 후에도 로그인 상태를 유지하려면 앱이 시작될 때 supabase에서 세션을 가져와 zustand에 설정해줘야 한다.
그리고 후에 사용할 PrivateRoute처럼 로그인 여부에 따른 접근을 제어하는 로직이 있을 때는 앱이 시작될 때 유저 상태가 정확히 설정되어야 제대로 작동한다.
이 함수는 앱이 처음 시작될 때 호출하면 된다. 예를 들어,
// src/App.tsx
import { useEffect } from 'react';
import { initializeUser } from './utils/initAuth';
import { useUserStore } from './store/userStore';
const App = () => {
const user = useUserStore((state) => state.user);
useEffect(() => {
initializeUser();
}, []);
return (
<div>
{user ? <p>Welcome, {user.email}</p> : <p>Please log in.</p>}
{/* 나머지 앱 로직 */}
</div>
);
};
export default App;
App.tsx나 최상위 컴포넌트에 useEffect를 사용해 호출하면 된다.
이렇게 하면 최상위 컴포넌트가 처음 렌더링될 때 한번 실행되면서 supabase에서 로그인된 유저 정보를 가져와 zustand store에 저장한다. 이후 다른 페이지나 컴포넌트에서 전역 상태인 user 값을 통해 로그인 여부를 확인하거나 유저 정보를 사용할 수 있다.
페이지 별 로그인 접근 권한 구현을 하려는데 위에 로직에 문제가 있었다..!
1. 훅 규칙을 지키지 않음 멍츙멍츙....
2. 페이지 이동 로직의 중첩으로 인한 문제
구현 하려 했던 내용은
비로그인 : url 링크로 마이페이지 접근 시도 시 로그인 페이지로 이동, 로그인 버튼 클릭 시 로그인 페이지로 이동
로그아웃 : 로그아웃 버튼 클릭 시 로그인 페이지가 아닌 메인 페이지로 이동
이었는데 비로그인 시 마이페이지로 이동하려고 하면 로그인페이지 이동이 안되거나 로그아웃 버튼을 누르면 로그인페이지로 넘어가는 현상이 발생했다.
위 문제들을 차근차근 수정해보자 !
1. store에 로그인 유지를 위한 상태를 추가하기
import { create } from 'zustand';
import { User } from '@supabase/supabase-js';
interface UserState {
user: User | null;
setUser: (user: User | null) => void;
clearUser: () => void;
isLoggingOut : boolean;
setIsLoggingOut : (isLoggingOut: boolean) => void;
isInitialized: boolean;
setIsInitialized: (isInitialized: boolean) => void;
}
export const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
isLoggingOut : false,
setIsLoggingOut : (isLoggingOut) => set({ isLoggingOut }),
isInitialized: false,
setIsInitialized: (isInitialized) => set({isInitialized}),
}));
로그인 유저 데이터의 상태관리와 별개로 유지하기 위한 상태 (initialized, setInitialized)를 추가했다.
또 로그아웃을 위한 isLoggingOut 상태도 추가하였다.
2. hook 수정하기
import { useEffect } from 'react';
import { useUserStore } from '../store/userStore';
import supabase from '../supabase/supabaseClient';
export const useUserInitialize = async () => {
const { setUser, setIsInitialized } = useUserStore.getState();
useEffect(() => {
const initUser = async () => {
const { data } = await supabase.auth.getUser();
if (data?.user) {
setUser(data.user);
}
setIsInitialized(true);
}
initUser();
}, [setUser, setIsInitialized])
};
3. PrivateRoute 수정하기
import { Outlet, useNavigate } from "react-router-dom";
import { useUserStore } from "../store/userStore";
import { useEffect } from "react";
import LoginHome from "../login-signup/Login";
export default function PrivateRoute() {
const user = useUserStore((state) => state.user);
const isLoggingOut = useUserStore((state) => state.isLoggingOut);
const navigate = useNavigate();
useEffect(() => {
if (!user && isLoggingOut) {
navigate("/loginhome");
}
if (isLoggingOut === true) {
navigate("/");
}
}, [user, isLoggingOut]);
return user ? <Outlet/> : <LoginHome/>;
}
//Mypage.tsx
//생략...
const signOut = async (): Promise<void> => {
await supabase.auth.signOut();
clearUser();
setIsLoggingOut(true);
};
//생략...
PrivateRoute에 로그인하면 얻을 수 있는 user 데이터와 로그아웃 여부를 구분할 수 있는 isLoggingOut을 가져온다.
비로그인과 로그인 이후 따로 로그아웃 하지 않은 상태에서는 로그인 페이지로 넘어가고,
로그아웃 버튼을 누르면 isLoggingOut이 true가 되면서 메인페이지로 이동하게 했다.
4. 최상위 컴포넌트에 로그인 유지 로직 수정
import "./App.css";
import { Outlet, useLocation } from "react-router-dom";
import { useUserStore } from "./store/userStore";
import { useUserInitialize } from "./utils/initAuth";
import Loading from "./components/Loading";
import AddButton from "./components/AddButton";
import Layout from "./Layout";
import Header from "./header/Header";
function App() {
const isInitialized = useUserStore((state) => state.isInitialized);
useUserInitialize();
const { pathname } = useLocation();
const { user } = useUserStore.getState();
const hideAddButton =
pathname === "/login" || pathname === "/signup" || pathname === "/add";
const hideHeader = pathname === "/login" || pathname === "/signup";
if (!isInitialized) {
return <Loading />;
}
return (
<>
<Layout>
{!hideHeader && <Header />}
{user && !hideAddButton && <AddButton />}
<Outlet />
</Layout>
</>
);
}
export default App;
이때 마이페이지에서 새로고침을 하면 로그인 페이지가 잠깐 보이는 불필요한 렌더링이 일어났다.
zustand로 상태를 관리하니 상태가 바뀔 때마다 렌더링이 일어날 수 밖에 없었다.
그래서 로그인을 유지할 유저 데이터가 없으면 로딩 컴포넌트를 넣어줬다.
이렇게 하면 새로고침해도 마이페이지가 바로 나올 수 있게 된다!
나중에 더 좋은 방법이 있다면 수정해봐야겠다.
React-Query를 사용하자! useAuth를 기본으로 제공하여 로그인, 로그아웃 관리가 편리하다
정리는 차차 해보기로!
'React & TypeScript' 카테고리의 다른 글
[TIL] zustand 사용 시 state.state와 getState()의 차이 (0) | 2024.11.17 |
---|---|
[React] input 값을 useState로 상태 관리 할 때 defaultValue를 왜 사용하면 안될까? (0) | 2024.11.15 |
[TIL] tailwindcss 설치하기 (1) | 2024.10.21 |
[TIL] Hydration 이란? (0) | 2024.08.20 |
[TIL] React에서 key가 필요한 이유가 뭘까? (1) | 2024.07.24 |