티스토리 뷰
🚀 에러 핸들링 ?
리액트에 오류가 발생했을 때 유저 혹은 개발자에게 어떤 방식으로 표출할지에 대한 내용입니다.
여기서 오류는 api 요청에 대한 오류 메시지일 수도 있고, 리액트에서 캐치한 오류일 수도 있습니다. 또한 에러가 발생할법한 상황들을 생각해보고 유저에게 에러 발생을 알리는 것 뿐만 아니라, 가능하다면 어떻게 대응할지도 전달해주는 것이 사용성 측면에서도 좋다고 볼 수 있습니다.
어떤 식으로 에러를 알리는가 ?
유저에게는 toast UI를 통해 에러 상황을 알려주거나 상황에 따라서는 다시시도 버튼 혹은 로그인 모달을 제공할 수도 있겠습니다. 또한 개발자에게 개발 환경에서는 에러 발생 시 console.error()로 알려주고, 배포 환경에서 에러 발생시 Sentry로 알려주어 대응합니다. 위와 같은 모든 상황은 '에러'가 발생했을 때의 로직을 작성함으로써 이루어집니다.
에러가 발생할법한 상황 ?
- get 요청이 실패한 상황
- 페이지에 "서비스에 접속할 수 없습니다. 새로고침을 하거나 잠시후 다시 접속해 주시기 바랍니다."라는 문구와 새로고침 버튼이 있는 컴포넌트 보여주기
- 데이터 변경 (post, put, delete) 요청이 실패한 상황
- 페이지는 유지하되, 다시 시도해달라고 toast UI 보여주기
- 그 외 api 에러(500대 서버 에러)
- 페이지에 "예상치 못한 에러가 발생했습니다. 잠시후 다시 시도해주세요."라는 문구와 새로고침이 버튼이 있는 컴포넌트 보여주기
- 요청 권한이 없는 상황 (ex. 만료된 토큰, 토큰이 없음)
- "접근 권한이 없습니다." 라는 문구와 로그인 form이 있는 컴포넌트 보여주기
- 없는 페이지에 접근했을 때
- 404 페이지를 보여주기
어떻게 구현할까
React Query에서는 에러를 핸들링할 수 있는 세 개의 방법이 있습니다.
- useQeury로부터 반환한 error property
- onError 콜백(query에서 직접 선언하거나 global QueryCache / MutationCache)
- Error boundaries 사용
React-Query에서 제공하는 global callbacks를 이용하여 에러가 발생할 때마다 toast를 표출하고, Error Boundary를 이용하여 에러가 발생한 경우 대체 페이지(fallback UI)를 보여주는 것을 처리해 봅시다.
또한 API의 에러 응답 코드 별로 나눠서 상황에 따른 메세지를 주어 에러를 핸들링해봅시다.
용어
fallback
어떤 기능이 제대로 동작하지 않을 때, 이에 대처하는 기능 또는 동작을 말합니다. 실패에 대해서 후처리를 위해 설정해 두는 것을 의미합니다.
Suspend
loading과 비슷한 의미를 지닙니다.
Error Boundary
Error Boundary란 React v16에 도입된 에러를 핸들링할 수 있는 React 합성 컴포넌트입니다. Error Boundary는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하고, 에러가 발생한 컴포넌트 트리 대신 fallback UI를 보여줍니다. 쉽게 말하면 에러가 나면 fallback이라는 속성에 넣어준 컴포넌트를 보여주게 됩니다.
Error Boundary는 에러를 핸들링하는 방법입니다. 기존에 우리는 try-catch문을 이용하여 에러를 핸들링하곤 했습니다. 이둘의 차이점은 무엇일까요?
try {
showButton();
} catch (error) {
// ...
}
try-catch는 명령적으로 어떻게 에러를 핸들링할지 집중하는 방법입니다. try로 감싼 곳에서 에러가 발생하면 catch에서 에러를 잡아 핸들링해줍니다. 그러나 Error Boundary는 선언적으로 핸들링이 가능합니다.
직접 Error boundary class를 만들고 커스터마이징하는 방법과 React-Error-Boundary라는 모듈을 같이 이용해 보려고 합니다.
🚀 react-error-boundary 라이브러리로 에러 핸들링
기본 사용 방법
<ErrorBoundary fallback={<div>에러발생 !</div>}
onError={(error) => 에러 상황에 따른 에러 표출 로직}
>
<UserList />
</ErrorVoundary>
위와 같이 작성하면 ErrorBoundary 안에 있는 children에서 에러가 발생하면 children이 ErrorBoundary에게 에러를 던져서(throw) fallback에 있는 컴포넌트가 화면에 보여지게 됩니다.
Error Boundary class 커스터마이징
📜 apis/@error.js 파일
class ApiCustomError extends Error {
constructor(message, status) {
super(message);
this.status = status;
if (status === 403) {
// 로그아웃 로직
window.location.href = '/';
this.message = '세션이 만료되었습니다.';
}
if (status === 404) {
// 404 페이지로 이동하는 로직
this.message = '존재하지 않는 페이지입니다.';
}
// ...
}
}
export default ApiCustomError;
위 코드와 같이 Error를 상속받아서 에러 코드에 따라 처리하고 싶은 로직을 작성해주면 됩니다.
그 후에 다시 App.js로 돌아와 아래와 같이 작성해 주면 됩니다.
📜 App.js 파일
function App() {
return(
<ErrorBoundary
fallback={<div>에러발생!!</div>}
onError={(error) => {
const { response } = error;
const err = new ApiCustomError(response.data, response.status);
alert(err);
}}
>
<UserList />
</ErrorBoundary>
)
}
ErrorBoundary 내부 상태를 재설정하는 방법
ErrorBoundary 내부에서 오류가 발생한 경우 해당 오류를 복구하고 사용자가 "다시 시도" 하거나 작업을 계속할 수 있도록 하려면 ErrorBoundary의 내부 상태를 재설정하는 방법이 필요합니다.
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>Something went wrong :(</p>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>다시시도</button>
</div>
);
}
function Bomb() {
throw new Error('에러 발생🚨🚨');
}
function ErrorBoundaryIndex() {
const [error, setError] = useState(false);
return (
<>
<button onClick={() => setError((prev) => !prev)}>
에러 발생 버튼!!
</button>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setError(false)}
resetKeys={[error]}
>
{error ? <Bomb /> : null}
</ErrorBoundary>
</>
);
}
export default ErrorBoundaryIndex;
에러가 발생하게 되면 유저가 원래 작업을 이어서 할 수 있도록 에러가 나기 전 상태로 돌아가도록 합니다.
🚀 React-Query에서 에러 핸들링
1) React-Error-Boundary + React-Query
1. queryClient에서 useErrorBoundary 옵션 설정하기
useQuery에서 data를 로드할 때, 발생한 에러를 렌더 단계에서 에러를 발생시키고 가장 가까운 오류 경계로 던지려면 useErrorBoundary 옵션을 true로 해주면 됩니다.
📜 App.js 파일
import { QueryClient, QueryClientProvider } from 'react-query'
function App() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
useErrorBoundary: true,
},
},
});
return (
<QueryClientProvider clinet={queryClient}>
// ...
</QueryClientProvider>
)
}
2. 구체적으로 어떤 컴포넌트 위에서 에러를 핸드링하고 싶을 때
한 페이지의 데이터를 가져오지 못한 상황인데 모든 페이지가 보이지 않는다면 사용자 측면에서 불편할 수 있다고 생각이 듭니다. 그래서 각각의 페이지에 Error Boundary를 적용하여 데이터를 가져오지 못했을 때, 하위 컴포넌트에서 발생하는 쿼리 오류들을 재설정합니다. 만약 정의된 QueryErrorResetBoundary 컴포넌트가 없다면 전역으로 설정됩니다.
function Page() {
const { reset } = useQueryErrorResetBoundary();
return(
<ErrorBoundary
onReset={reset}
FallbackComponent={ErrorFallback}
message="사용자를 로드하는데 실패 하였습니다."
>
<Profile />
</ErrorBoundary>
)
}
하지만 위와 같이 커스텀 없이 사용하게 되면 상황에 따라 에러를 핸들링 하는 것이 아니라 모든 에러에 대해서 같은 message와 FallbackComponent를 보여주게 되는 것입니다.
생각해보면, 에러의 종류에 따라 사용자에게도 정확하게 이게 무슨 상황인지를 알려주고, 그에 맞는 대응 방법을 알려주는 것은 사용자 경험 측면에서도 중요할 것 같습니다.
그러기 위해서는 에러 처리 흐름을 커스텀해야만 합니다.
2) 에러 처리 Hook (커스텀 훅) 으로 에러 핸들링
에러 처리 흐름의 주요한 부분들은 Hook에서 모두 담당하고, 개별적인 컴포넌트에서는 정말 딱 추가적으로 정의해야 하는 로직에만 집중합니다. 아래 예시 코드에서는 api 요청 시 에러가 발생했을 때를 핸들링하는 hook이므로 useApiError라고 했습니다.
1. 커스텀 훅 만들기
Hook 내부에서는 상황별로 어떤 에러 핸들러를 수행할지 결정하는 부분이 가장 중요합니다. 그 결정은 아래 조건에 따릅니다.
- 에러 발생 시 실행한 가능성이 있는 핸들러는 5가지가 있습니다. 그리고 나열 순서가 실행 우선 순위입니다.
- 컴포넌트에서 (HTTP status, 서비스 표준 에러 Code) Key 조합으로 재정의한 핸들러
- 컴포넌트에서 (HTTP Status) Key로 재정의한 핸들러
- Hook에서 (HTTP Status, 서비스 표준 에러 Code) Key 조합으로 정의한 핸들러
- Hook에서 (HTTP Status) Key로 정의한 핸들러
- 어디에서도 정의되지 못한 에러를 처리하는 핸들러
위 조건을 예시 코드로 작성해보면 아래와 같습니다.
우선 전역적으로 사용할 에러 핸들 로직은 따로 파일로 관리했습니다.
📜 Utils/ErrorHandler/handler.js 파일
export const defaultHandler = () => {
// 전역에서 사용할 에러 핸들 로직
alert('defaultHandler');
};
// 401번 에러가 발생했다면
export const defaultErrorHandler401 = () => {
// 전역에서 사용할 에러 핸들 로직
alert('401에러입니다.');
};
export const defaultErrorHandler404 = () => {
alert('404에러입니다.');
};
// 500번 에러가 발생했다면
export const defaultErrorHandler500 = () => {
// 전역에서 사용할 에러 핸들 로직
alert('500에러 입니다.');
};
📜 useApiError.js 파일
import { useCallback } from 'react';
import {
defaultErrorHandler401,
defaultErrorHandler404,
defaultErrorHandler500,
defaultHandler,
} from '../Utils/ErrorHandler/handler';
// 기본 핸들러 예시. 특정 HTTP Status와 서비스 표준 에러 Code 일 때 전역적으로 적용하기로 사전 정의한 핸들러들입니다.
const defaultHandlers = {
default: defaultHandler,
401: {
default: defaultErrorHandler401,
},
404: {
default: defaultErrorHandler404,
},
500: {
default: defaultErrorHandler500,
},
};
// 매개변수 handlers: 컴포넌트에서 재정의한 핸들러 모음
const useApiError = (handlers) => {
// ...
// 우선순위에 따른 핸들러의 선택과 실행
const handleError = useCallback(
(error) => {
// console.log(error); // 꼭 에러를 콘솔에 찍어보고 Status 코드가 어디에 있는지 확인하자
const httpStatus = error.response.status; // HTTP Status
while (true) {
if (httpStatus && handlers[httpStatus]) {
handlers[httpStatus].default();
break;
} else if (defaultHandlers[httpStatus]) {
defaultHandlers[httpStatus].default();
break;
} else {
defaultHandlers.default();
}
}
},
[handlers]
);
// ...
return { handleError };
};
export default useApiError;
2. 컴포넌트에 따라 핸들러 재정의
개별 컴포넌트에서 같은 에러 코드이지만, 전역적으로 설정해둔 에러 핸들 로직 말고 따로 컴포넌트에 따라 다른 에러 핸들 로직을 사용하고 싶을 수 있습니다.
개별 컴포넌트에서 특정 HTTP Status에 따라 실행할 로직을 지정하고 싶다면 Hook을 사용할 때 인자로 Handler 함수를 전달합니다. 그러면 우리가 Hook에서 우선순위에 따라 작성한 로직에 의해 전역적으로 설정한 hanlder보다 재정의된 handler가 먼저 case문에 걸려 결과적으로는 개별 컴포넌트에서 재정의한 handler
import { useQuery } from 'react-query';
import UserApi from '../../Apis/userApi';
import useApiError from '../useApiError';
const detailErrorHandler401 = () => {
alert('개별 컴포넌트에서 재정의한 401에러 핸들러');
};
const detailErrorHandler404 = () => {
alert('개별 컴포넌트에서 재정의한 404에러 핸들러');
};
const detailErrorHandler500 = () => {
alert('개별 컴포넌트에서 재정의한 500에러 핸들러');
};
// 개별 컴포넌트에서 에러를 핸들링하려면 onError에 핸들러를 적어준다
const useGetUser = () => {
const { handleError } = useApiError({
401: {
default: detailErrorHandler401,
},
404: {
default: detailErrorHandler404,
},
500: {
default: detailErrorHandler500,
},
});
const getUser = async () => {
const res = await UserApi.getUser();
return res;
};
const { data, error, status, isLoading } = useQuery('key', getUser, {
refetchWindowFocus: false,
retry: 1,
onSuccess: () => {},
onError: handleError,
});
return { data, error, status, isLoading };
};
export default useGetUser;
React Query의 onError의 값으로는 기본적으로 QueryClient를 생성할 때 설정한 랜들러가 사용되지만, useQuery나 useMutaion 훅을 사용할 때 option으로 onError에 새로운 핸들러를 설정하면 기본값을 덮어씁니다. 그래서 위와 같이 에러 처리 Hook을 사용하면 모든 에러에 기본적인 에러 처리 흐름을 적용하면서 재정의가 필요한 부분만 상황에 따라 추가할 수 있습니다.
3. React Query의 QueryClient를 초기화할 때 핸들링에 사용할 Hook 전달
위에서 커스텀 훅을 만들 때 매개변수 자리에 hanlders를 넣었었는데, 이건 개별 컴포넌트에서 핸들러 재정의가 필요해 재정의한 랜들러 모음을 전달했을 경우가 됩니다. 따라서 전역에서 따로 핸들러 재정의 없이 즉, 매개변수를 전달하지 않아도 커스텀 훅 내에서 defaultHandlers를 정의 해두었고, 재정의된 hanlders가 오지 않으면 우선순위에 따라 defaultHandlers에 정의된 로직이 실행될 것입니다.
📜 App.js 파일
import { QueryClient, QueryClientProvider } from 'react-query';
import UserList from '../useRef/UserList';
function QueryErrorHandlingIndex() {
const { handleError } = useApiError();
const queryClient = new QueryClient({
defaultOptions: {
onError: handleError,
},
});
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
export default QueryErrorHandlingIndex;
같이 보면 좋을 site
https://blog.hwahae.co.kr/all/tech/7867
https://www.npmjs.com/package/react-error-boundary
https://velog.io/@suyeon9456/React-Query-Error-Boundary-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
'프론트엔드 > React' 카테고리의 다른 글
[React] React-Query 낙관적 업데이트 (0) | 2023.03.28 |
---|---|
[React] 데이터를 받아오는 동안을 Suspense로 처리해보자 (0) | 2023.03.27 |
[React] React-Query로 서버 데이터의 상태를 전역으로 관리하자 (0) | 2023.03.20 |
[React] Recoil로 전역 상태 관리를 하자 (0) | 2023.03.20 |
[React] Redux-Toolkit를 알아보자🪄 줄여서 RTK (0) | 2023.03.14 |
- Total
- Today
- Yesterday
- frontend
- 프로젝트 회고
- 머신러닝
- testing
- 자바
- 리액트
- 자바스크립트 기초
- react-query
- 인프런
- 디프만
- JSP
- 파이썬
- react
- next.js
- rtl
- HTML
- 스타일 컴포넌트 styled-components
- styled-components
- 리액트 훅
- 프론트엔드 공부
- Python
- CSS
- 자바스크립트
- TypeScript
- 프론트엔드 기초
- 데이터분석
- 딥러닝
- 타입스크립트
- jest
- 프론트엔드
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |