Skip to content

Commit ecab9ae

Browse files
authored
fix : api reissue 오류 수정 (#152)
* refactor : api reissue 오류 수정 * refactor : SSE 토큰값 업데이트 후 헤더에 토큰값 안 담기는 현상 수정(함수 안에서 전역 토큰값 사용)
1 parent a19e460 commit ecab9ae

File tree

4 files changed

+55
-57
lines changed

4 files changed

+55
-57
lines changed

src/apis/client.ts

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,11 @@ import axios from 'axios';
22

33
import useAuthStore from '@/stores/authStore';
44

5-
import { getNewToken } from './auth';
6-
75
const client = axios.create({
86
baseURL: import.meta.env.VITE_API_URL,
97
headers: { 'Content-Type': 'application/json' },
108
});
119

12-
let isRefreshing = false;
13-
14-
const callReissue = async () => {
15-
try {
16-
const response = await getNewToken();
17-
if(response?.status !== 200) throw new Error('error while fetching newToken');
18-
const newToken = response?.data.data.accessToken;
19-
return newToken;
20-
} catch (e) {
21-
return Promise.reject(e);
22-
}
23-
};
24-
25-
let retry = false;
26-
2710
client.interceptors.request.use(
2811
(config) => {
2912
const accessToken = useAuthStore.getState().accessToken;
@@ -39,36 +22,31 @@ client.interceptors.request.use(
3922
client.interceptors.response.use(
4023
(response) => response,
4124
async (error) => {
42-
const setAccessToken = useAuthStore.getState().setAccessToken;
4325
const logout = useAuthStore.getState().logout;
4426
const isLoggedIn = useAuthStore.getState().isLoggedIn;
4527

4628
const originalRequest = error.config;
4729

48-
if (!originalRequest || originalRequest.url === '/auth/reissue') {
30+
if (!originalRequest || originalRequest.url === '/api/reissue') {
4931
if (isLoggedIn) logout();
5032
return Promise.reject(error);
5133
}
5234

53-
if ((error.response?.status === 401 || error.response?.status === 403) && !retry) {
54-
retry = true;
55-
if (isRefreshing) {
56-
if (isLoggedIn) logout();
57-
} else {
58-
isRefreshing = true;
59-
try {
60-
const newToken = await callReissue();
61-
setAccessToken(newToken);
62-
isRefreshing = false;
63-
originalRequest.headers.Authorization = `Bearer ${newToken}`;
64-
return client(originalRequest);
65-
} catch (e) {
66-
isRefreshing = false;
67-
if (isLoggedIn) logout();
68-
return Promise.reject(e);
69-
}
35+
if (
36+
(error.response?.status === 401 || error.response?.status === 403) &&
37+
!originalRequest._retry
38+
) {
39+
originalRequest._retry = true;
40+
41+
try {
42+
const newToken = await useAuthStore.getState().refreshToken();
43+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
44+
return client(originalRequest);
45+
} catch (e) {
46+
return Promise.reject(e);
7047
}
7148
}
49+
7250
return Promise.reject(error);
7351
},
7452
);

src/hooks/useServerSentEvents.tsx

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import useAuthStore from '@/stores/authStore';
55
import useToastStore from '@/stores/toastStore';
66
import { useNavigate } from 'react-router';
77
import useNotificationStore from '@/stores/notificationStore';
8-
import { getNewToken } from '@/apis/auth';
98

109
interface MessageEventData {
1110
title: string;
@@ -19,7 +18,6 @@ export const useServerSentEvents = () => {
1918
// const recallCountRef = useRef(1);
2019

2120
const accessToken = useAuthStore((state) => state.accessToken);
22-
const setAccessToken = useAuthStore((state) => state.setAccessToken);
2321
const sourceRef = useRef<EventSourcePolyfill | null>(null);
2422

2523
const setToastActive = useToastStore((state) => state.setToastActive);
@@ -41,27 +39,17 @@ export const useServerSentEvents = () => {
4139
}
4240
};
4341

44-
// 토큰 재발급 함수
45-
const callReissue = async () => {
46-
try {
47-
const response = await getNewToken();
48-
if (response?.status !== 200) throw new Error('error while fetching newToken');
49-
const newToken = response?.data.data.accessToken;
50-
return setAccessToken(newToken);
51-
} catch (e) {
52-
return Promise.reject(e);
53-
}
54-
};
55-
5642
useEffect(() => {
5743
if (!accessToken) {
58-
console.log('로그인 정보 확인불가');
44+
// console.log('로그인 정보 확인불가');
5945
return;
6046
}
6147

6248
const connectSSE = () => {
49+
const accessToken = useAuthStore.getState().accessToken;
50+
6351
try {
64-
console.log('구독 시작');
52+
// console.log('구독 시작');
6553
sourceRef.current = new EventSourcePolyfill(
6654
`${import.meta.env.VITE_API_URL}/api/notifications/sub`,
6755
{
@@ -77,11 +65,15 @@ export const useServerSentEvents = () => {
7765
handleOnMessage(event.data);
7866
};
7967

80-
sourceRef.current.onerror = (event) => {
68+
sourceRef.current.onerror = async (event) => {
8169
console.log(event);
8270
const errorEvent = event as unknown as { status?: number };
8371
if (errorEvent.status === 401) {
84-
callReissue();
72+
try {
73+
await useAuthStore.getState().refreshToken();
74+
} catch (error) {
75+
console.log('다른 api에서 리프레시 토큰 호출중입니다.');
76+
}
8577
closeSSE();
8678
reconnect = setTimeout(connectSSE, 5000);
8779
} else {
@@ -97,7 +89,7 @@ export const useServerSentEvents = () => {
9789
connectSSE();
9890

9991
return () => {
100-
console.log('컴포넌트 언마운트로 인한 구독해제');
92+
// console.log('컴포넌트 언마운트로 인한 구독해제');
10193
closeSSE();
10294
};
10395
}, [accessToken]);

src/layouts/PrivateRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useServerSentEvents } from '@/hooks/useServerSentEvents';
66
import Toast from '@/components/Toast';
77

88
export default function PrivateRoute() {
9-
useServerSentEvents();
9+
// useServerSentEvents();
1010
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
1111
const navigate = useNavigate();
1212

src/stores/authStore.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { create } from 'zustand';
22
import { persist, createJSONStorage } from 'zustand/middleware';
33
import useThemeStore from './themeStore';
4+
import { getNewToken } from '@/apis/auth';
45

56
interface AuthStore {
67
isLoggedIn: boolean;
@@ -12,11 +13,14 @@ interface AuthStore {
1213
setZipCode: (zipCode: string) => void;
1314
setAccessToken: (accessToken: string) => void;
1415
setIsAdmin: () => void;
16+
isRefreshing: boolean;
17+
refreshPromise: Promise<string> | null;
18+
refreshToken: () => Promise<string>;
1519
}
1620

1721
const useAuthStore = create(
1822
persist<AuthStore>(
19-
(set) => ({
23+
(set, get) => ({
2024
isLoggedIn: false,
2125
accessToken: '',
2226
zipCode: '',
@@ -40,6 +44,30 @@ const useAuthStore = create(
4044
setZipCode: (zipCode) => set({ zipCode: zipCode }),
4145
setAccessToken: (accessToken) => set({ accessToken: accessToken }),
4246
setIsAdmin: () => set({ isAdmin: true }),
47+
isRefreshing: false,
48+
refreshPromise: null,
49+
refreshToken: async () => {
50+
// 이미 재발급 중이면 진행 중인 Promise 반환
51+
if (get().isRefreshing && get().refreshPromise) {
52+
return get().refreshPromise;
53+
}
54+
const refreshPromise = getNewToken()
55+
.then((response) => {
56+
if (response?.status !== 200) throw new Error('Token refresh failed');
57+
const newToken = response?.data.data.accessToken;
58+
set({ accessToken: newToken, isRefreshing: false, refreshPromise: null });
59+
return newToken;
60+
})
61+
.catch((error) => {
62+
set({ isRefreshing: false, refreshPromise: null });
63+
// 재발급 실패 시 로그아웃
64+
if (get().isLoggedIn) get().logout();
65+
throw error;
66+
});
67+
68+
set({ isRefreshing: true, refreshPromise });
69+
return refreshPromise;
70+
},
4371
}),
4472
{
4573
name: 'userInfo',

0 commit comments

Comments
 (0)