Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…aerval-Dadok-FE into refactor/#603
  • Loading branch information
hanyugeon committed May 28, 2024
2 parents 67f600e + 59aa92c commit 4c09ccb
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 65 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"private": true,
"scripts": {
"dev": "next dev -H local.dev.dadok.app",
"dev-ssl": "node scripts/server.js local.dev.dadok.app",
"build": "next build",
"start": "next start -H local.dev.dadok.app",
"dev-ssl": "node scripts/server.js local.dev.dadok.app",
"start-ssl": "NODE_ENV=production node scripts/server.js local.dev.dadok.app",
"lint": "next lint",
"prepare": "husky install",
"storybook": "storybook dev -p 6006",
Expand Down
46 changes: 18 additions & 28 deletions src/apis/core/axios.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -38,10 +38,6 @@ const requestHandler = (config: InternalAxiosRequestConfig) => {
return config;
};

/** api 요청이 병렬적으로 이뤄질 때,
* 토큰 업데이트는 한번만 요청하기 위해 사용되는 flag 변수 */
let isRefreshing = false;

const responseHandler = async (error: unknown) => {
if (isAxiosErrorWithCustomCode(error)) {
const { config: originRequest, response } = error;
Expand All @@ -50,7 +46,7 @@ const responseHandler = async (error: unknown) => {

console.warn(code, message);

if (originRequest && isAuthRefreshError(code) && !isRefreshing) {
if (originRequest && isAuthRefreshError(code)) {
return silentRefresh(originRequest);
}

Expand All @@ -66,45 +62,39 @@ 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);
}
};

const updateToken = async () => {
try {
const {
data: { accessToken },
} = await axios.post<{ accessToken: string }>('/service-api/auth/token');
/** api 요청이 병렬적으로 이뤄질 때,
* 토큰 업데이트는 한번만 요청하기 위해 사용되는 flag 변수 */
let isTokenRefreshing = false;

if (!accessToken) {
throw new Error('새로운 accessToken을 받아오지 못했어요.');
const updateToken = () =>
new Promise<string>((resolve, reject) => {
if (isTokenRefreshing) {
reject(new AuthRefreshIgnoredError('Already trying to refresh token'));
return;
}

return accessToken;
} catch (error) {
return Promise.reject(error);
}
};
isTokenRefreshing = true;

axios
.post<{ accessToken: string }>('/service-api/auth/token')
.then(({ data }) => resolve(data.accessToken))
.catch(reason => reject(reason))
.finally(() => (isTokenRefreshing = false));
});

const removeToken = () => {
storage.remove();

if (isClient()) {
window.location.reload();
}
};

const setAxiosAuthHeader = (
Expand Down
29 changes: 0 additions & 29 deletions src/app/error.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'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 (
<html>
<body>
<div className="absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center gap-[2rem]">
<Image
src="/images/loading.gif"
width={230}
height={160}
alt="loading"
/>
<div className="font-heading">
<span className="font-bold text-main-900">다독이</span>도 몰라요~ 왜
이래요~
</div>
<Button
size="large"
colorScheme="main"
fill={false}
onClick={() => router.replace('/')}
>
처음으로 돌아가기
</Button>
</div>
</body>
</html>
);
};

export default ErrorPage;
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ContextProvider from '@/components/ContextProvider';
import AuthFailedErrorBoundary from '@/components/AuthFailedErrorBoundary';
import Layout from '@/v1/layout/Layout';

import { LineSeedKR } from '@/styles/font';
Expand All @@ -18,7 +19,9 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
{/* @todo Chakra 제거시 app-layout 프로퍼티 제거. */}
<body className={`${LineSeedKR.variable} app-layout font-lineseed`}>
<Layout>
<ContextProvider>{children}</ContextProvider>
<ContextProvider>
<AuthFailedErrorBoundary>{children}</AuthFailedErrorBoundary>
</ContextProvider>
</Layout>
</body>
</html>
Expand Down
43 changes: 43 additions & 0 deletions src/components/AuthFailedErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'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 (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} FallbackComponent={AuthFailedFallback}>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
};

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 <Loading fullpage />;
};
6 changes: 1 addition & 5 deletions src/components/ContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
'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';
Expand All @@ -14,9 +12,7 @@ const ContextProvider = ({ children }: { children: ReactNode }) => {
<RecoilRoot>
<ReactQueryProvider>
<ChakraThemeProvider>
<ErrorBoundary fallbackRender={ErrorPage}>
<ToastProvider>{children}</ToastProvider>
</ErrorBoundary>
<ToastProvider>{children}</ToastProvider>
</ChakraThemeProvider>
</ReactQueryProvider>
</RecoilRoot>
Expand Down
10 changes: 9 additions & 1 deletion src/components/ReactQueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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';
Expand All @@ -14,11 +15,18 @@ const ReactQueryProvider: NextPage<PropTypes> = ({ children }) => {
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
retry: (_count, error) => {
if (error instanceof AuthRefreshIgnoredError) {
return true;
}

return false;
},
},
},
})
);

return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
Expand Down
11 changes: 11 additions & 0 deletions src/types/customError/AuthRefreshIgnoredError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* accessToken을 갱신하는 요청이 진행 중인 경우, 갱신 요청은 무시되고 해당 에러가 발생합니다.
*/
class AuthRefreshIgnoredError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}

export default AuthRefreshIgnoredError;
1 change: 1 addition & 0 deletions src/types/customError/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AuthRefreshIgnoredError } from './AuthRefreshIgnoredError';

0 comments on commit 4c09ccb

Please sign in to comment.