🐸 공지사항
context로 하는게 가장 베스트였습니다.
zustand로 변경할 수도있겠지만 유저 로그인후 상태 관리는 어차피 user state하나만 있는거라
zustand는 오히려 오버스펙일수도 있겠다는 생각이 듭니다.
그리고 추가로 인증된 사용자만 들어올수있는 페이지나 게시글 작성시에는
requireAuth 같은 훅을 따로 써서 처리를해주면 될것같습니다
글을 지울까 했으나 쓴 시간이 아까워 그냥둡니다..ㅠㅠ
0. 배경
현재 계약직으로 있는 회사에서 회원가입,로그인 기능을 내가 구현하지 못해 아쉬움이 있어서
supabase,nextjs로 supabase template을 만든 개인 토이 프로젝트를 했었다.
회사에서는 로그인 및 회원가입 supabase로 인증 처리후 contextAPI status에 담아 리턴해주는 식이였는데
이 방식도 괜찮지만 contextAPI는 user값이 변경 될 때마다 재랜더링이 된다는 점이 좀 걸렸다.
물론 context에 저장된 user값은 로그인 로그아웃 할 때만 바뀌기 때문에 재랜더링 걱정은 없고, 따로 user 인증 페이지같은 경우 currenctUser() 훅 같은걸로 인증 처리를 해준다던가 하는식으로 하면 문제 될건 없었지만
개인적으로 조금 더 나은 방향이 없을까 고민하던중 zustand를 도입해봤다.
1. zustand 도입
'use client';
import { useEffect } from 'react';
import { createClient } from '@/utils/supabase/client';
import { useUserStore } from '@/store/userStore';
export function AuthStateManager({ children }: { children: React.ReactNode }) {
const supabase = createClient();
const { setLoading, setUser } = useUserStore();
useEffect(() => {
setLoading(true);
supabase.auth.getSession().then(({ data: { session } }) => {
if (session?.user) {
supabase
.from('members')
.select('*')
.eq('id', session.user.id)
.single()
.then(({ data }) => {
setUser(data);
setLoading(false);
});
}
});
setLoading(false);
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
setUser(null);
}
setTimeout(async () => {
if (session?.user) {
const { data } = await supabase
.from('members')
.select('*')
.eq('id', session.user.id)
.single();
setUser(data);
}
}, 0);
});
return () => subscription.unsubscribe();
}, [setUser, setLoading, supabase]);
return children;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
return <AuthStateManager>{children}</AuthStateManager>;
}
코드는 딱히 별거 없다. 짧막하게 설명하자면,
- supabase.auth.getSession()
- 현재 사용자의 세션 정보를 한 번만 가져오고,
- 앱이 처음 로드될 때 사용자가 이미 로그인되어 있는지 확인하는 용도다.
- supabase.auth.onAuthStateChange()
- 인증 상태가 변경될 때마다 실행되는 이벤트 리스너다. 실시간으로 사용자 상태를 동기화 하는데 사용된다.
- ex)사용자가 로그인할 때, 로그아웃 할 때, 토큰 갱신될 때 등등..
이 두 가지를 함께 사용하는 이유는:
- getSession()으로 초기 상태를 가져오고
- onAuthStateChange()로 이후의 모든 변경사항을 감지하여
- 항상 최신의 인증 상태를 유지할 수 있기 때문이다.
요약하자면, 인증을 완료하고 주스탠드 스토어에 유저 데이터를 집어넣는것이다.
이 방식도 꽤나 만족스러웠지만 여러곳에 발품을 팔아 의견을 종합해본 결과,
이 방식도 단점이 존재했다.
( setTimeout(0)을 사용했다는 점이나 복잡성 등등은 제쳐두고..)
먼저,
1. 새로고침시 데이터 손실 - 사소함 어차피 인증 관련 로직은 FM으로하면 페이지 변경때마다 인증하는게 맞음
2. 보안 측면 - 이 경우는 사실 와닿지 않는다. 프로텍트 페이지같은경우 따로 훅을 작성해서 인증해주면 되지않나 싶다.
1번과 2번은 솔직히 실제로는 큰 문제가 되지 않을것 같고, 오히려 자연스러운 웹 애플리케이션의 동작 방식에 가깝다는 생각이 든다.
그렇지만 내가 코드를 사용해보면서 zustand에서 react-query 방식으로 변경한 결정적인 이유 다음과 같다.
3. 어떤 건 Zustand로 관리하고 어떤 건 React Query로 관리하는지 기준이 모호해졌다.
간단히 예를들면 이렇다.
// 어떤 컴포넌트에서
const { user } = useUserStore(); // 여기서 유저 정보 가져오고
const { data: posts } = useQuery(['posts', user.id], () => fetchUserPosts(user.id)); // 여기서 게시글 가져오고
const { data: comments } = useQuery(['comments', user.id], () => fetchUserComments(user.id)); // 댓글도 가져오고
// ... 이런식으로 계속
그래서 리액트쿼리로 리펙토링을 해줬다.
2. react-query
먼저 AuthProvider.tsx 코드는 다음과 같이 바꿔주었다.
'use client'
import { useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useUserStore } from '@/store/userStore'
import { useQueryClient } from '@tanstack/react-query'
export function AuthProvider({ children }: React.PropsWithChildren) {
const supabase = createClient()
const queryClient = useQueryClient()
useEffect(() => {
// 초기 인증 상태 동기화
const initializeAuth = async () => {
const {
data: { session },
} = await supabase.auth.getSession()
if (session?.user) {
queryClient.invalidateQueries({ queryKey: ['auth'] })
}
}
initializeAuth()
// 인증 상태 변경 구독
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
queryClient.invalidateQueries({ queryKey: ['auth'] })
} else if (event === 'SIGNED_OUT') {
queryClient.setQueryData(['auth'], null)
}
})
return () => subscription.unsubscribe()
}, [queryClient, supabase])
return (children)
}
여기서 포인트는 다음과 같다.
1. auth 쿼리키는 굳이 관리할 필요가 없어서 하드코딩해줬다.
2. getSession()과 onAuthStateChange()는 그래도 사용해야한다.
다음은 zustand에서 react-query로 변경한 useAuth 로직이다.
import { SupabaseClient } from '@supabase/supabase-js'
import { Tables } from '@/types/supabase'
import { useQuery } from '@tanstack/react-query'
export const useAuth = (supabase: SupabaseClient) => {
return useQuery<Tables<'members'> | null>({
queryKey: ['auth'],
queryFn: async (): Promise<Tables<'members'> | null> => {
const {
data: { session },
} = await supabase.auth.getSession()
if (!session?.user) return null
const { data } = await supabase
.from('members')
.select('*')
.eq('id', session.user.id)
.single()
return data
},
staleTime: 5 * 60 * 1000,
gcTime: 5 * 60 * 1000,
retry: 1,
refetchOnWindowFocus: true,
})
}
포인트는 다음과 같다.
1. refetchOnWindowFocus를 true로 줌으로써 브라우저가 포커스될 때마다 재인증하게 해줬다.
(이로 인해 프로텍트페이지에 진입한 유저가 다른 탭에서 놀다가 다시 현재 탭으로 돌아올 경우 재인증처리를 진행해준다.)
2. 5분뒤 다시 재인증 처리를 진행한다.
3. supabaseClient는 외부에서 주입받는다. (서버컴포넌트 혹은 클라이언트컴포넌트 어디서 사용될지 모르기때문에.. 유연성을위해서)
'기록' 카테고리의 다른 글
'Set'을 활용해서 기존 배열중에서 삭제된 배열값 구하기 (0) | 2024.11.19 |
---|---|
'parameter' , 'argument' 정의 (2) | 2024.11.18 |
react, nestjs에서 소셜 로그인 구현하기 (0) | 2024.08.19 |