목차
프로젝트를 진행하면서 에러를 상단에 묶어서 처리한 과정들을 정리하고자 한다.
이렇게 하면 디테일한 에러처리 부분에서 문제가 될 수 있지만 대부분의 에러는 처리할 수 있다.
에러 발생시 화면 구성 방안
ErrorSection
페이지가 렌더링 될 때 데이터를 불러와 화면을 그려야 하는 경우에는 데이터를 불러오는 동안 화면에 로딩을 보여줘야 하기 때문에, 리액트 쿼리의 error boundary를 통해 오류가 생길 때 화면을 정의해 준다.
ErrorModal
API 요청 실패가 화면에 영향을 주지 않은 경우, 오류 메세지를 표시할 필요가 없으면 ErrorModal을 사용한다.
text
API 요청 실패 시, 단순히 오류 메세지를 띄워야 하며 현재 화면에 영향을 주면 안되는 경우에는 string 타입의 오류 메세지를 화면에 표시한다.
get 요청에서의 쿼리에러 바운더리를 통한 에러 처리
쿼리에러 바운더리의 내부 작동 원리
쿼리에러 바운더리 로직을 보면 알 수 있다.
기본적으로 render 함수에서 children을 return 하고 에러 발생 시 fallback ui를 return 한다.
class ErrorBoundary extends Component{
constructor(props){
super(props)
this.state = { hasError: false};
}
static getDrivedStateFromError(error){
return { hasError: true};
}
render(){
if (this.state.hasError){
return <h1>에러가 발생했어요</h1>;
}
return this.props.children;
}
}
적용 방법
queryClient 기본 설정하기
throwOnError true를 해주어야 최상단 error boundary 로 전파가 된다.
import { getErrorDataByCode } from "@/api/getErrorDataByCode";
import { QueryClient } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "react-toastify";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000 * 60,
gcTime: 30 * 1000 * 60,
throwOnError: true,
notifyOnChangeProps: ["data"],
},
mutations: {
throwOnError: false,
onError: (error) => {
if (axios.isAxiosError(error)) {
const errorData = getErrorDataByCode(error);
toast.error(`[${errorData.code}] ${errorData.message}`);
} else {
toast.error("알 수 없는 에러가 발생했습니다.");
}
},
},
},
});
쿼리에러바운더리를 만든 코드
import { useQueryErrorResetBoundary } from "@tanstack/react-query";
import { ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
import ErrorIcon from "@/assets/Error.svg?react";
import { useLocation } from "react-router-dom";
export default function QueryErrorBoundary({
children,
}: {
children: ReactNode;
}) {
const { reset } = useQueryErrorResetBoundary();
const location = useLocation();
return (
<ErrorBoundary
key={location.pathname}
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "100%",
textAlign: "center",
}}
>
<ErrorIcon
style={{ width: "70px", height: "auto", marginTop: "200px" }}
/>
<h2
style={{
fontSize: "1.2rem",
fontWeight: "bold",
marginBottom: "15px",
}}
>
오류 발생!
</h2>
<button
onClick={resetErrorBoundary}
style={{
backgroundColor: "black",
color: "white",
padding: "10px 20px",
border: "none",
borderRadius: "5px",
fontSize: "1rem",
cursor: "pointer",
}}
>
↻ 다시 시도
</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}
만든 바운더리 적용하기
아래와 같이 상단의 레이아웃에 감싸주었다.
import { Outlet } from "react-router-dom";
import styled from "styled-components";
import QueryErrorBoundary from "@/services/QueryErrorBoundary";
import { Suspense } from "react";
import Loading from "@/pages/Loading";
import Header from "./Header";
import Footer from "./Footer";
const Background = styled.div`
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
`;
const Main = styled.main`
flex: 1;
overflow: hidden;
`;
function CommonLayout() {
return (
<Background>
<Header />
<Main>
<QueryErrorBoundary>
<Suspense
fallback={
<Loading
title="데이터를 불러오는 중.."
description="열심히 데이터를 가져오고 있어요"
/>
}
>
<Outlet />
</Suspense>
</QueryErrorBoundary>
</Main>
<Footer />
</Background>
);
}
export default CommonLayout;
기존에 한 페이지에서 오류가 발생하면 다른 페이지에도 오류 페이지가 전달되는 문제가 있었다.
그래서 에러 바운더리에 key값을 넣어주니 해결할 수 있었다. 키 값으로 재랜더링이 진행된다.
쿼리 에러 바운더리의 한계점
다음과 같이 한 화면에서 api를 요청하는 부분이 두개 이상 있을 경우 한 곳에서 에러가 발생하면
에러가 최상단의 에러 바운더리로 전파되어 전체 화면 ui가 바뀌게 된다.
따라서 부분적인 곳만 오류가 나게끔 하는 조정을 하기 위해서는 에러 바운더리를 세분화 시키거나
쓰지 않고 isError을 통한 조건부 컴포넌트 랜더링을 해주어야 한다.
get 요청을 제외한 부분 error 공통으로 toast 메세지 설정하기
아래 코드 예시는 아직 api를 연결하지 않아 mocking으로 진행하였다.
deleteData를 예시 코드로 가져왔다.
axios.isAxiosError를 이용해 에러를 그대로 던져주면 된다.
import { ApiResponse } from "@/types/ApiResponse";
import axios, { AxiosRequestConfig } from "axios";
import api from ".";
/**
*
* @param url
* @param config
* @returns
*/
const deleteData = async <T>(
url: string,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> => {
try {
const { data } = await api.delete<ApiResponse<T>>(url, config);
return data;
} catch (error) {
console.error(error);
if (axios.isAxiosError(error)) {
throw error;
}
throw new Error("Unknown error occurred");
}
};
export default deleteData;
이런식으로 변형해서 error로 주게 되면 다음의 로직들에서 문제가 되니 위와 같이 쓰자
//잘못된 로직
if (error instanceof AxiosError) {
throw new Error(error.message || "API 요청 실패");
}
에러 데이터 파싱 함수
import { AxiosError } from 'axios';
type ErrorCodeType = {
[key: string]: { code: string; message: string; requireLogin?: boolean };
};
export const ERROR_CODE: ErrorCodeType = {
default: { code: 'ERROR', message: '알 수 없는 오류가 발생했습니다.' },
// axios error
ERR_NETWORK: { code: '통신 에러', message: '서버가 응답하지 않습니다. \n프로그램을 재시작하거나 관리자에게 연락하세요.' },
ECONNABORTED: { code: '요청 시간 초과', message: '요청 시간을 초과했습니다.' },
// http status code 및 정의 된 코드
400: { code: '400', message: '잘못된 요청.' },
4001: { code: '4001', message: '요청에 대한 Validation 에러입니다.' },
401: { code: '401', message: '인증 에러.', requireLogin: true },
4011: { code: '4011', message: '인증이 만료되었습니다.', requireLogin: true },
403: { code: '403', message: '권한이 없습니다.' },
} as const;
export const getErrorDataByCode = (error: AxiosError<{ code: number; message: string }>) => {
const serverErrorCode = error?.response?.data?.code ?? '';
const httpErrorCode = error?.response?.status ?? '';
const axiosErrorCode = error?.code ?? '';
if (serverErrorCode in ERROR_CODE) {
return ERROR_CODE[serverErrorCode ];
}
if (httpErrorCode in ERROR_CODE) {
return ERROR_CODE[httpErrorCode];
}
if (axiosErrorCode in ERROR_CODE) {
return ERROR_CODE[axiosErrorCode];
}
return ERROR_CODE.default;
};
queryClient 공통 mutate 설정에 토스트 로직 추가하기
이때 위의 에러 데이터 파싱 함수를 써주자
import { getErrorDataByCode } from "@/api/getErrorDataByCode";
import { QueryClient } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "react-toastify";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000 * 60,
gcTime: 30 * 1000 * 60,
throwOnError: true,
notifyOnChangeProps: ["data"],
},
mutations: {
throwOnError: false,
onError: (error) => {
if (axios.isAxiosError(error)) {
const errorData = getErrorDataByCode(error);
toast.error(`[${errorData.code}] ${errorData.message}`);
} else {
toast.error("알 수 없는 에러가 발생했습니다.");
}
},
},
},
});
msw를 통해 삭제 시 에러 전달하기
http.delete(`${import.meta.env.VITE_API_URL}/room/remove/1`, async () => {
return HttpResponse.json( { code: 401, message: "삭제할 수 없는 방입니다." },
{ status: 401 })
}),
참고
[React] ErrorBoundary & Suspense, 거의 완벽한 사용방법 가이드
📒 ErrorBoundary & Suspense, 거의 완벽한 사용방법 가이드잘 만든 Errorboundary백개의 try-catch 안 부럽다.프론트 개발을 계속하다 보니 다양한 상황을 마주치게 되었고, 그에 따른 적절한 화면 표현의 중
lasbe.tistory.com
'코딩 정보 > React' 카테고리의 다른 글
[React] React-Dom vs React의 차이점 (0) | 2025.04.12 |
---|---|
[React] 소켓 통신 전 개념에 대해 정리해보자 (0) | 2025.04.11 |
리액트 리팩토링 정리글 (2) | 2025.03.28 |
[vite][vitest] 프로젝트 중 테스트 코드를 작성하며.. (0) | 2025.02.26 |
[React][Zustand] SessionStorage를 사용해 보자 (0) | 2024.12.08 |