Skip to content

Commit

Permalink
feat: 에러 처리 보강 및 쿠키 기반 제거
Browse files Browse the repository at this point in the history
  • Loading branch information
alstn2468 committed Dec 15, 2024
1 parent 93ab66d commit 345f014
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 184 deletions.
34 changes: 24 additions & 10 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
"date-fns": "^3.3.1",
"framer-motion": "^11.2.10",
"jotai": "^2.8.3",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-daum-postcode": "^3.1.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.50.0",
"react-intersection-observer": "^9.8.0",
"react-pdf": "^9.0.0",
Expand Down
68 changes: 3 additions & 65 deletions apps/admin/src/atoms/useAuthAtom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Cookies from 'js-cookie';
import { LOCAL_STORAGE, COOKIES } from '@boolti/api';
import { LOCAL_STORAGE } from '@boolti/api';
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';

const storageMethod = {
getItem: (key: string, initialValue: string | null) => {
Expand All @@ -15,41 +13,9 @@ const storageMethod = {
},
};

const accessTokenAtom = atom<string | null>(
(() => {
const accessTokenFromCookie = Cookies.get(COOKIES.ACCESS_TOKEN);
const accessTokenFromStorage = storageMethod.getItem(LOCAL_STORAGE.ACCESS_TOKEN, null);
const accessTokenAtom = atom<string | null>(null);

if (accessTokenFromCookie) {
localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, accessTokenFromCookie);
return accessTokenFromCookie;
}

if (accessTokenFromStorage) {
return accessTokenFromStorage;
}

return null;
})(),
);

const refreshTokenAtom = atom<string | null>(
(() => {
const refreshTokenFromCookie = Cookies.get(COOKIES.ACCESS_TOKEN);
const refreshTokenFromStorage = storageMethod.getItem(LOCAL_STORAGE.REFRESH_TOKEN, null);

if (refreshTokenFromCookie) {
localStorage.setItem(LOCAL_STORAGE.REFRESH_TOKEN, refreshTokenFromCookie);
return refreshTokenFromCookie;
}

if (refreshTokenFromStorage) {
return refreshTokenFromStorage;
}

return null;
})(),
);
const refreshTokenAtom = atom<string | null>(null);

export const useAuthAtom = () => {
const [accessToken, setAccessToken] = useAtom(accessTokenAtom);
Expand All @@ -65,40 +31,12 @@ export const useAuthAtom = () => {
const removeToken = () => {
storageMethod.removeItem(LOCAL_STORAGE.ACCESS_TOKEN);
storageMethod.removeItem(LOCAL_STORAGE.REFRESH_TOKEN);
Cookies.remove(COOKIES.ACCESS_TOKEN);
Cookies.remove(COOKIES.REFRESH_TOKEN);
setAccessToken(null);
setRefreshToken(null);
};

const isLogin = () => !!accessToken && !!refreshToken;

useEffect(() => {
const handler = ({ key, newValue }: StorageEvent) => {
switch (key) {
case LOCAL_STORAGE.ACCESS_TOKEN: {
setAccessToken(newValue);
newValue
? Cookies.set(COOKIES.ACCESS_TOKEN, newValue)
: Cookies.remove(COOKIES.ACCESS_TOKEN);
return;
}
case LOCAL_STORAGE.REFRESH_TOKEN: {
setRefreshToken(newValue);
newValue
? Cookies.set(COOKIES.REFRESH_TOKEN, newValue)
: Cookies.remove(COOKIES.REFRESH_TOKEN);
return;
}
}
};
window.addEventListener('storage', handler);

return () => {
window.removeEventListener('storage', handler);
};
}, [setAccessToken, setRefreshToken]);

return {
setToken,
removeToken,
Expand Down
92 changes: 53 additions & 39 deletions apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
import { BooltiHTTPError, LOCAL_STORAGE } from '@boolti/api';
import React from 'react';
import { Navigate } from 'react-router-dom';
import {
BooltiHttpError,
BooltiHttpErrorParams,
LOCAL_STORAGE,
checkIsAuthError,
checkIsHttpError,
} from '@boolti/api';
import { useNavigate } from 'react-router-dom';

import { PATH } from '../../constants/routes';

interface AuthErrorBoundaryProps {
children?: React.ReactNode;
}

interface AuthErrorBoundaryState {
status: BooltiHTTPError['status'] | null;
}

const initialState: AuthErrorBoundaryState = {
status: null,
};

class AuthErrorBoundary extends React.Component<AuthErrorBoundaryProps, AuthErrorBoundaryState> {
public state: AuthErrorBoundaryState = initialState;

public static getDerivedStateFromError(error: Error): AuthErrorBoundaryState {
if (error instanceof BooltiHTTPError) {
return {
status: error.status,
};
}

return {
status: null,
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { checkIsWebView, isWebViewBridgeAvailable, requestToken } from '@boolti/bridge';
import { useEffect } from 'react';

const AuthErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
const navigate = useNavigate();

useEffect(() => {
const reset = async () => {
if (checkIsAuthError(error)) {
if (checkIsWebView() && isWebViewBridgeAvailable()) {
const token = (await requestToken()).data.token;
localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, token);
resetErrorBoundary();
} else {
navigate(PATH.LOGIN, { replace: true });
}
} else {
if (checkIsHttpError(error)) {
let customOptions: BooltiHttpErrorParams['customOptions'];
try {
const body = await error.response.json();
customOptions = {
errorTraceId: body.errorTraceId,
type: body.type,
detail: body.detail,
};
} catch {
throw new BooltiHttpError({
request: error.request,
response: error.response,
options: error.options,
customOptions,
});
}
}
navigate(PATH.HOME, { replace: true });
}
};
}

public render() {
if (this.state.status !== null) {
this.setState(initialState);

window.localStorage.removeItem(LOCAL_STORAGE.ACCESS_TOKEN);
window.localStorage.removeItem(LOCAL_STORAGE.REFRESH_TOKEN);
reset();
}, []);

Check warning on line 53 in apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

React Hook useEffect has missing dependencies: 'error', 'navigate', and 'resetErrorBoundary'. Either include them or remove the dependency array. If 'resetErrorBoundary' changes too often, find the parent component that defines it and wrap that definition in useCallback

return <Navigate to={PATH.LOGIN} replace />;
}
return null;
};

return this.props.children;
}
}
const AuthErrorBoundary = ({ children }: React.PropsWithChildren) => {
return <ErrorBoundary FallbackComponent={AuthErrorFallback}>{children}</ErrorBoundary>;
};

export default AuthErrorBoundary;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isBooltiHTTPError } from '@boolti/api/src/BooltiHTTPError';
import { checkIsBooltiHttpError } from '@boolti/api';
import { useEffect } from 'react';
import { Navigate, useRouteError } from 'react-router-dom';
import { PATH } from '~/constants/routes';
Expand All @@ -7,7 +7,7 @@ const GlobalErrorBoundary = () => {
const error = useRouteError();

useEffect(() => {
if (error instanceof Error && isBooltiHTTPError(error)) {
if (error instanceof Error && checkIsBooltiHttpError(error)) {
const errorMessage = '[BooltiHTTPError] errorTraceId:' + error.errorTraceId + '\n';
'[BooltiHTTPError] type' + error.type + '\n';
'[BooltiHTTPError] detail' + error.detail;
Expand Down
26 changes: 11 additions & 15 deletions packages/api/src/BooltiHTTPError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,32 @@ import { HTTPError } from 'ky';

import { ERROR_CODE } from './constants';

interface BooltiHTTPErrorOptions {
interface BooltiHttpErrorOptions {
errorTraceId: string;
type: keyof typeof ERROR_CODE;
detail: string;
}

class BooltiHTTPError extends HTTPError {
export interface BooltiHttpErrorParams {
response: Response;
request: Request;
options: NormalizedOptions;
customOptions?: BooltiHttpErrorOptions;
}

export class BooltiHttpError extends HTTPError {
public errorTraceId?: string;
public type?: keyof typeof ERROR_CODE;
public detail?: string;
public status: number;

constructor(
response: Response,
request: Request,
options: NormalizedOptions,
customOptions?: BooltiHTTPErrorOptions,
) {
constructor({ request, response, options, customOptions }: BooltiHttpErrorParams) {
super(response, request, options);

this.name = 'BooltiHTTPError';
this.name = 'BooltiHttpError';
this.errorTraceId = customOptions?.errorTraceId;
this.type = customOptions?.type;
this.detail = customOptions?.detail;
this.status = response.status;
}
}

export function isBooltiHTTPError(error: Error): error is BooltiHTTPError {
return error.name === 'BooltiHTTPError';
}

export default BooltiHTTPError;
9 changes: 1 addition & 8 deletions packages/api/src/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { QueryClient, QueryClientProvider as BaseQueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

import BooltiHTTPError from './BooltiHTTPError';

export function QueryClientProvider({ children }: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
Expand All @@ -12,12 +10,7 @@ export function QueryClientProvider({ children }: React.PropsWithChildren) {
refetchOnWindowFocus: false,
retry: false,
staleTime: 5000,
useErrorBoundary: (error) => {
// 인증 관련 에러일 때만 ErrorBoundary를 사용한다.
return (
error instanceof BooltiHTTPError && (error.status === 401 || error.status === 403)
);
},
useErrorBoundary: true,
},
},
}),
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/constants/errorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ export const ERROR_CODE = {
type: 'TOKEN_REFRESH_FAILED',
status: 400,
},
UNAUTHROIZED: {
type: 'UNAUTHROIZED',
status: 401,
},
FORBIDDEN: {
type: 'FORBIDDEN',
status: 403,
},
};
4 changes: 2 additions & 2 deletions packages/api/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ERROR_CODE } from './errorCode';
import { LOCAL_STORAGE, COOKIES } from './storages';
import { LOCAL_STORAGE } from './storages';

export { ERROR_CODE, LOCAL_STORAGE, COOKIES };
export { ERROR_CODE, LOCAL_STORAGE };
5 changes: 0 additions & 5 deletions packages/api/src/constants/storages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,3 @@ export const LOCAL_STORAGE = {
ACCESS_TOKEN: 'accessToken',
REFRESH_TOKEN: 'refreshToken',
};

export const COOKIES = {
ACCESS_TOKEN: 'x-access-token',
REFRESH_TOKEN: 'x-refresh-token',
};
Loading

0 comments on commit 345f014

Please sign in to comment.