diff --git a/src/apis/client.ts b/src/apis/client.ts index 426b7c4..8ed72ee 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -2,28 +2,11 @@ import axios from 'axios'; import useAuthStore from '@/stores/authStore'; -import { getNewToken } from './auth'; - const client = axios.create({ baseURL: import.meta.env.VITE_API_URL, headers: { 'Content-Type': 'application/json' }, }); -let isRefreshing = false; - -const callReissue = async () => { - try { - const response = await getNewToken(); - if(response?.status !== 200) throw new Error('error while fetching newToken'); - const newToken = response?.data.data.accessToken; - return newToken; - } catch (e) { - return Promise.reject(e); - } -}; - -let retry = false; - client.interceptors.request.use( (config) => { const accessToken = useAuthStore.getState().accessToken; @@ -39,36 +22,31 @@ client.interceptors.request.use( client.interceptors.response.use( (response) => response, async (error) => { - const setAccessToken = useAuthStore.getState().setAccessToken; const logout = useAuthStore.getState().logout; const isLoggedIn = useAuthStore.getState().isLoggedIn; const originalRequest = error.config; - if (!originalRequest || originalRequest.url === '/auth/reissue') { + if (!originalRequest || originalRequest.url === '/api/reissue') { if (isLoggedIn) logout(); return Promise.reject(error); } - if ((error.response?.status === 401 || error.response?.status === 403) && !retry) { - retry = true; - if (isRefreshing) { - if (isLoggedIn) logout(); - } else { - isRefreshing = true; - try { - const newToken = await callReissue(); - setAccessToken(newToken); - isRefreshing = false; - originalRequest.headers.Authorization = `Bearer ${newToken}`; - return client(originalRequest); - } catch (e) { - isRefreshing = false; - if (isLoggedIn) logout(); - return Promise.reject(e); - } + if ( + (error.response?.status === 401 || error.response?.status === 403) && + !originalRequest._retry + ) { + originalRequest._retry = true; + + try { + const newToken = await useAuthStore.getState().refreshToken(); + originalRequest.headers.Authorization = `Bearer ${newToken}`; + return client(originalRequest); + } catch (e) { + return Promise.reject(e); } } + return Promise.reject(error); }, ); diff --git a/src/hooks/useServerSentEvents.tsx b/src/hooks/useServerSentEvents.tsx index 3795fa9..fc2fc5d 100644 --- a/src/hooks/useServerSentEvents.tsx +++ b/src/hooks/useServerSentEvents.tsx @@ -5,7 +5,6 @@ import useAuthStore from '@/stores/authStore'; import useToastStore from '@/stores/toastStore'; import { useNavigate } from 'react-router'; import useNotificationStore from '@/stores/notificationStore'; -import { getNewToken } from '@/apis/auth'; interface MessageEventData { title: string; @@ -19,7 +18,6 @@ export const useServerSentEvents = () => { // const recallCountRef = useRef(1); const accessToken = useAuthStore((state) => state.accessToken); - const setAccessToken = useAuthStore((state) => state.setAccessToken); const sourceRef = useRef(null); const setToastActive = useToastStore((state) => state.setToastActive); @@ -41,27 +39,17 @@ export const useServerSentEvents = () => { } }; - // 토큰 재발급 함수 - const callReissue = async () => { - try { - const response = await getNewToken(); - if (response?.status !== 200) throw new Error('error while fetching newToken'); - const newToken = response?.data.data.accessToken; - return setAccessToken(newToken); - } catch (e) { - return Promise.reject(e); - } - }; - useEffect(() => { if (!accessToken) { - console.log('로그인 정보 확인불가'); + // console.log('로그인 정보 확인불가'); return; } const connectSSE = () => { + const accessToken = useAuthStore.getState().accessToken; + try { - console.log('구독 시작'); + // console.log('구독 시작'); sourceRef.current = new EventSourcePolyfill( `${import.meta.env.VITE_API_URL}/api/notifications/sub`, { @@ -77,11 +65,15 @@ export const useServerSentEvents = () => { handleOnMessage(event.data); }; - sourceRef.current.onerror = (event) => { + sourceRef.current.onerror = async (event) => { console.log(event); const errorEvent = event as unknown as { status?: number }; if (errorEvent.status === 401) { - callReissue(); + try { + await useAuthStore.getState().refreshToken(); + } catch (error) { + console.log('다른 api에서 리프레시 토큰 호출중입니다.'); + } closeSSE(); reconnect = setTimeout(connectSSE, 5000); } else { @@ -97,7 +89,7 @@ export const useServerSentEvents = () => { connectSSE(); return () => { - console.log('컴포넌트 언마운트로 인한 구독해제'); + // console.log('컴포넌트 언마운트로 인한 구독해제'); closeSSE(); }; }, [accessToken]); diff --git a/src/layouts/PrivateRoute.tsx b/src/layouts/PrivateRoute.tsx index fd9a26d..95fb2fd 100644 --- a/src/layouts/PrivateRoute.tsx +++ b/src/layouts/PrivateRoute.tsx @@ -6,7 +6,7 @@ import { useServerSentEvents } from '@/hooks/useServerSentEvents'; import Toast from '@/components/Toast'; export default function PrivateRoute() { - useServerSentEvents(); + // useServerSentEvents(); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); const navigate = useNavigate(); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 7557b97..9b5530a 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import useThemeStore from './themeStore'; +import { getNewToken } from '@/apis/auth'; interface AuthStore { isLoggedIn: boolean; @@ -12,11 +13,14 @@ interface AuthStore { setZipCode: (zipCode: string) => void; setAccessToken: (accessToken: string) => void; setIsAdmin: () => void; + isRefreshing: boolean; + refreshPromise: Promise | null; + refreshToken: () => Promise; } const useAuthStore = create( persist( - (set) => ({ + (set, get) => ({ isLoggedIn: false, accessToken: '', zipCode: '', @@ -40,6 +44,30 @@ const useAuthStore = create( setZipCode: (zipCode) => set({ zipCode: zipCode }), setAccessToken: (accessToken) => set({ accessToken: accessToken }), setIsAdmin: () => set({ isAdmin: true }), + isRefreshing: false, + refreshPromise: null, + refreshToken: async () => { + // 이미 재발급 중이면 진행 중인 Promise 반환 + if (get().isRefreshing && get().refreshPromise) { + return get().refreshPromise; + } + const refreshPromise = getNewToken() + .then((response) => { + if (response?.status !== 200) throw new Error('Token refresh failed'); + const newToken = response?.data.data.accessToken; + set({ accessToken: newToken, isRefreshing: false, refreshPromise: null }); + return newToken; + }) + .catch((error) => { + set({ isRefreshing: false, refreshPromise: null }); + // 재발급 실패 시 로그아웃 + if (get().isLoggedIn) get().logout(); + throw error; + }); + + set({ isRefreshing: true, refreshPromise }); + return refreshPromise; + }, }), { name: 'userInfo',