diff --git a/src/apis/core/axios.ts b/src/apis/core/axios.ts index 5f5a8b4a..25951871 100644 --- a/src/apis/core/axios.ts +++ b/src/apis/core/axios.ts @@ -1,12 +1,12 @@ import axios, { CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; -import { AuthRefreshIgnoredError } from '@/types/customError'; import { ACCESS_TOKEN_STORAGE_KEY, SERVICE_ERROR_MESSAGE } from '@/constants'; import { isAuthFailedError, isAuthRefreshError, isAxiosErrorWithCustomCode, } from '@/utils/helpers'; +import isClient from '@/utils/isClient'; import webStorage from '@/utils/storage'; const storage = webStorage(ACCESS_TOKEN_STORAGE_KEY); @@ -38,6 +38,10 @@ const requestHandler = (config: InternalAxiosRequestConfig) => { return config; }; +/** api 요청이 병렬적으로 이뤄질 때, + * 토큰 업데이트는 한번만 요청하기 위해 사용되는 flag 변수 */ +let isRefreshing = false; + const responseHandler = async (error: unknown) => { if (isAxiosErrorWithCustomCode(error)) { const { config: originRequest, response } = error; @@ -46,7 +50,7 @@ const responseHandler = async (error: unknown) => { console.warn(code, message); - if (originRequest && isAuthRefreshError(code)) { + if (originRequest && isAuthRefreshError(code) && !isRefreshing) { return silentRefresh(originRequest); } @@ -62,39 +66,45 @@ const responseHandler = async (error: unknown) => { const silentRefresh = async (originRequest: InternalAxiosRequestConfig) => { try { + isRefreshing = true; + const newToken = await updateToken(); storage.set(newToken); setAxiosAuthHeader(originRequest, newToken); + isRefreshing = false; + return await publicApi(originRequest); } catch (error) { removeToken(); + isRefreshing = false; + return Promise.reject(error); } }; -/** api 요청이 병렬적으로 이뤄질 때, - * 토큰 업데이트는 한번만 요청하기 위해 사용되는 flag 변수 */ -let isTokenRefreshing = false; +const updateToken = async () => { + try { + const { + data: { accessToken }, + } = await axios.post<{ accessToken: string }>('/service-api/auth/token'); -const updateToken = () => - new Promise((resolve, reject) => { - if (isTokenRefreshing) { - reject(new AuthRefreshIgnoredError('Already trying to refresh token')); - return; + if (!accessToken) { + throw new Error('새로운 accessToken을 받아오지 못했어요.'); } - isTokenRefreshing = true; - - axios - .post<{ accessToken: string }>('/service-api/auth/token') - .then(({ data }) => resolve(data.accessToken)) - .catch(reason => reject(reason)) - .finally(() => (isTokenRefreshing = false)); - }); + return accessToken; + } catch (error) { + return Promise.reject(error); + } +}; const removeToken = () => { storage.remove(); + + if (isClient()) { + window.location.reload(); + } }; const setAxiosAuthHeader = ( diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 00000000..f2c65513 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,29 @@ +'use client'; + +import Button from '@/v1/base/Button'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +export const ErrorPage = () => { + const router = useRouter(); + + return ( +
+ loading +
+ 다독이도 몰라요~ 왜 + 이래요~ +
+ +
+ ); +}; + +export default ErrorPage; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx deleted file mode 100644 index c11a3ef5..00000000 --- a/src/app/global-error.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import Button from '@/v1/base/Button'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; - -export const ErrorPage = () => { - const router = useRouter(); - - return ( - - -
- loading -
- 다독이도 몰라요~ 왜 - 이래요~ -
- -
- - - ); -}; - -export default ErrorPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8e19e35d..4c2ed7e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,4 @@ import ContextProvider from '@/components/ContextProvider'; -import AuthFailedErrorBoundary from '@/components/AuthFailedErrorBoundary'; import Layout from '@/v1/layout/Layout'; import { LineSeedKR } from '@/styles/font'; @@ -19,9 +18,7 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => { {/* @todo Chakra 제거시 app-layout 프로퍼티 제거. */} - - {children} - + {children} diff --git a/src/components/AuthFailedErrorBoundary.tsx b/src/components/AuthFailedErrorBoundary.tsx deleted file mode 100644 index 3274a7c2..00000000 --- a/src/components/AuthFailedErrorBoundary.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { QueryErrorResetBoundary } from '@tanstack/react-query'; -import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; - -import useToast from '@/v1/base/Toast/useToast'; -import { isAuthFailedError, isAxiosErrorWithCustomCode } from '@/utils/helpers'; -import Loading from '@/v1/base/Loading'; - -const AuthFailedErrorBoundary = ({ - children, -}: { - children?: React.ReactNode; -}) => { - return ( - - {({ reset }) => ( - - {children} - - )} - - ); -}; - -export default AuthFailedErrorBoundary; - -const AuthFailedFallback = ({ error, resetErrorBoundary }: FallbackProps) => { - const { show: showToast } = useToast(); - - useEffect(() => { - if ( - isAxiosErrorWithCustomCode(error) && - isAuthFailedError(error.response.data.code) - ) { - showToast({ message: '다시 로그인 해주세요' }); - resetErrorBoundary(); - } - }, [error, resetErrorBoundary, showToast]); - - return ; -}; diff --git a/src/components/ContextProvider.tsx b/src/components/ContextProvider.tsx index 0bef940a..68edb417 100644 --- a/src/components/ContextProvider.tsx +++ b/src/components/ContextProvider.tsx @@ -1,8 +1,10 @@ 'use client'; import { ReactNode } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { RecoilRoot } from 'recoil'; +import ErrorPage from '@/app/error'; import ChakraThemeProvider from '@/components/ChakraThemeProvider'; import ReactQueryProvider from '@/components/ReactQueryProvider'; import ToastProvider from '@/v1/base/Toast/ToastProvider'; @@ -12,7 +14,9 @@ const ContextProvider = ({ children }: { children: ReactNode }) => { - {children} + + {children} + diff --git a/src/components/ReactQueryProvider.tsx b/src/components/ReactQueryProvider.tsx index 1871e422..fff2f458 100644 --- a/src/components/ReactQueryProvider.tsx +++ b/src/components/ReactQueryProvider.tsx @@ -1,4 +1,3 @@ -import AuthRefreshIgnoredError from '@/types/customError/AuthRefreshIgnoredError'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { NextPage } from 'next/types'; @@ -15,18 +14,11 @@ const ReactQueryProvider: NextPage = ({ children }) => { defaultOptions: { queries: { refetchOnWindowFocus: false, - retry: (_count, error) => { - if (error instanceof AuthRefreshIgnoredError) { - return true; - } - - return false; - }, + retry: false, }, }, }) ); - return ( diff --git a/src/types/customError/AuthRefreshIgnoredError.ts b/src/types/customError/AuthRefreshIgnoredError.ts deleted file mode 100644 index b85f0478..00000000 --- a/src/types/customError/AuthRefreshIgnoredError.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * accessToken을 갱신하는 요청이 진행 중인 경우, 갱신 요청은 무시되고 해당 에러가 발생합니다. - */ -class AuthRefreshIgnoredError extends Error { - constructor(message: string) { - super(message); - this.name = this.constructor.name; - } -} - -export default AuthRefreshIgnoredError; diff --git a/src/types/customError/index.ts b/src/types/customError/index.ts deleted file mode 100644 index 176fe8d4..00000000 --- a/src/types/customError/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AuthRefreshIgnoredError } from './AuthRefreshIgnoredError';