-
Notifications
You must be signed in to change notification settings - Fork 0
[FE] API 데이터 통신 에러 처리
YoonDH edited this page Feb 24, 2025
·
2 revisions
- axios를 사용하지 않기로 결정한 이후, fetch의 단점으로는 번거로움이 존재한다.
- HTTP 통신 메소드별로, 미리 사전에 코드를 생성하여 axios 처럼
.get .post같은 간단한 방식으로 API를 통신할 수 있도록 모듈화하였다.
export default function Fetch() {
const baseURL = import.meta.env.VITE_REACT_SERVER_BASE_URL;
const get = async <T>(url: string, params?: Record<string, string | number | boolean>): Promise<T> => {
console.log("GET : ", url);
const paramsURL = new URLSearchParams(
Object.entries(params || {}).map(([key, value]) => [key, String(value)]),
).toString();
const response = await fetch(`${baseURL}${url}?${paramsURL}`, {
method: "GET",
});
...
};
const post = async <T = boolean, K = unknown>(
url: string,
body?: Record<string, K | K[]>,
): Promise<T | boolean> => {
console.log("POST : ", url);
const response = await fetch(`${baseURL}${url}`, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
});
...
};
const put = async <T, K>(url: string, body?: Record<string, K>): Promise<T> => {
const response = await fetch(`${baseURL}${url}`, {
method: "PUT",
body: JSON.stringify(body),
});
...
};
return {
get,
post,
put,
};
}
const { get, post, put } = Fetch();
export { get as getFetch, post as postFetch, put as putFetch };- 사전에 메소드 호출 함수를 선언하여 이를 import하여 쓸 수 있도록 개발 편의성을 증진하였다.
| 의도한 에러 | 의도하지 않은 에러 |
|---|---|
| 잘못된 요청, 생성 실패, 조회 실패 | 서버 실패, 네트워크 오류 |
- 의도한 에러가 발생한 경우, 사용자에게 에러 메세지를 정보를 전달해야한다.
- 의도하지 않은 에러가 발생한 경우, 서버가 응답을 할 수 없는 형태로 간주하고 에러 페이지를 보여준다.

| 종류 | 적용 범위 | 특징 |
|---|---|---|
| useSuspenseQuery | get 요청 결과를 바로 화면에 그리는 경우 & 요청 과정 동안 로딩화면을 보여주고 싶은 경우 | 전역 Suspense를 통한 로딩화면, 오류 시 공통적인 오류 화면 |
| useQuery | get 요청 결과가 바로 화면에 그려지지 않는 경우 or 요청 과정을 사용자에게 다른 형태로 보여주고 싶은 경우 | 요청 성공 및 실패를 모달로 노출 |
| useMutation | post 요청의 결과는 화면에 반영되지 않거나 결과를 모달로 처리하는 경우 | POST 요청 성공 실패를 모달로 노출 |
- useSuspense는 API가 호출되는 동안, Suspense의 fallback 화면을 보여준다.
- 현재 서비스에서는 useSuspenseQuery에서 (빌딩, 위험 / 주의 요소)를 호출하기에, 이를 실패할 경우, 지도에 제공하는 정보가 없기에 에러 페이지로 이동한다.
- 어느 페이지에서든 useSuspenseQuery에서 오류가 발생할 경우, 하나의 에러 페이지를 보여주기 위해서 전역에서 에러 바운더리를 생성한다.
- react-error-boundary 라이브러리가 존재하나, 서비스에 맞게 커스텀의 가능성 및 번들 사이즈 감소를 위해 직접 구현하였다. Reference PR
import React, { Component, ReactNode } from 'react';
interface ErrorBoundaryProps {
fallback: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
export default class ErrorBoundary extends Component<
React.PropsWithChildren<ErrorBoundaryProps>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<ErrorBoundaryProps>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
<ErrorBoundary key={location.key} fallback={<ErrorPage />}>
...
</ErrorBoundary>- POST 요청의 결과를 화면에서 사용하지 않거나, 요청 실패에 대한 결과를 화면에 보여주는 로직이 반복 사용되었습니다.
- 기존에 요청 실패를 보여주기 위해서는
const {isError} = useMutation({});
if(isError) return <ErrorFallback/>
///
const {isError} = useMutation({
onError: (error) => {
setError(error);
}
})
if(error) return <ErrorFallback/>- 다음과 같이 에러가 발생했을 경우, 페이지 컴포넌트에서 isError 등 state를 사용하여 에러 화면을 만드는 로직이 필요하다.
- 현재 서비스에서 Mutation이 에러가 발생한 경우, 모두 동일한 모달 형태로 에러 메세지를 출력한다.
- 다음과 같은, 반복적인 로직이 발생하므로 코드의 간결성 및 책임 분리 원칙을 적용하여 커스텀 훅을 생성하였습니다.
type Fallback = {
[K in Exclude<ERROR_STATUS, ERROR_STATUS.INTERNAL_ERROR>]?: {
mainTitle: string;
subTitle: string[];
};
};
type HandleError = {
fallback: Fallback;
onClose?: () => void;
};
type UseMutationErrorReturn<TData, TError, TVariables, TContext> = [
React.FC,
UseMutationResult<TData, TError, TVariables, TContext>,
];
export default function useMutationError<TData, TError, TVariables, TContext>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
queryClient?: QueryClient,
handleError?: HandleError,
): UseMutationErrorReturn<TData, TError, TVariables, TContext> {
const [isOpen, setOpen] = useState<boolean>(false);
const result = useMutation<TData, TError, TVariables, TContext>(options, 1000, true, queryClient);
const { isError, error } = result;
useEffect(() => {
setOpen(isError);
}, [isError]);
const close = () => {
if (handleError?.onClose) handleError?.onClose();
setOpen(false);
};
const Modal: React.FC = () => {
if (!isOpen || !handleError || !error) return null;
const { fallback } = handleError;
let title: { mainTitle: string; subTitle: string[] } = {
mainTitle: "",
subTitle: [],
};
if (error instanceof NotFoundError) {
title = fallback[404] ?? title;
} else if (error instanceof BadRequestError) {
title = fallback[400] ?? title;
} else throw error;
return (
<div>
<div>
<div>
<p>{title.mainTitle}</p>
<div>
{title.subTitle.map((_subtitle, index) => (
<p>
{_subtitle}
</p>
))}
</div>
</div>
<button onClick={close}>
확인
</button>
</div>
</div>
);
};
return [Modal, result];
}- useMutation의 옵션을 그대로 받아서 Mutation의 결과와 실패했을 경우, 보여질 모달을 return한다.
- useMutationError를 선언할 때, fallback을 입력받는데, 이때 각 에러에서 보여질 모달의 문구를 입력받는다.
const [ErrorModal, { data: reportedRoute, mutate, status }] = useMutationError(
{
mutationFn: ({
startNodeId,
coordinates,
endNodeId,
}: {
startNodeId: NodeId;
endNodeId: NodeId | null;
coordinates: Coord[];
}) => postReportRoute(university.id, { startNodeId, endNodeId, coordinates }),
onSuccess: (data) => {
...
},
onError: () => {
...
},
},
undefined,
{
fallback: {
400: {
mainTitle: "경로를 생성하는데 실패하였습니다!",
subTitle: [
"선택하신 경로는 생성이 불가능합니다.",
"선 위에서 시작하여, 빈 곳을 이어주시기 바랍니다.",
],
},
404: {
mainTitle: "경로를 생성하는데 실패하였습니다!",
subTitle: ["선택하신 점이 관리자에 의해 제거되었습니다.", "다른 점을 선택하여 제보 부탁드립니다."],
},
},
},
);- 다음과 같이 400 에러에서는 어떤 문구를 보여주고 404에서는 어떤 문구를 보여줄지 정해진다면, 에러가 발생했을 경우, 해당 화면에서 모달과 함께 에러 타입에 맞는 메세지가 보이게 될 것이다.
| 오류 코드 | 예시 | 오류 코드 | 예시 |
|---|---|---|---|
| 길 생성 실패 400 | ![]() |
길 생성 실패 404 | ![]() |
- useQuery에서도 POST 요청과 마찬가지로, GET 요청의 결과가 화면에서 바로 사용되지 않는 경우에 적용할 수 있습니다.
- 현재 메인 서비스에서는 지도 메인페이지에서 경로를 탐색하고 성공하면
길 찾기 결과 페이지로 안내되고 실패한 경우 모달을 보여주기 위해 적용하였습니다.
type Fallback = {
[K in Exclude<ERROR_STATUS, ERROR_STATUS.INTERNAL_ERROR>]?: {
mainTitle: string;
subTitle: string[];
};
};
type HandleError = {
fallback: Fallback;
onClose?: () => void;
}
type UseQueryErrorReturn<TData, TError> = [
React.FC,
UseQueryResult<TData, TError>
];
export default function useQueryError<TQueryFnData, TError, TData>(
options: UseQueryOptions<TQueryFnData, TError, TData>,
queryClient?: QueryClient,
handleSuccess?: () => void,
handleError?: HandleError,
): UseQueryErrorReturn<TData, TError> {
const [isOpen, setOpen] = useState<boolean>(false);
const result = useQuery<TQueryFnData, TError, TData, readonly unknown[]>(options, queryClient);
const { isError, error, status } = result;
useEffect(() => {
setOpen(isError)
}, [isError])
useEffect(() => {
if (status === "success" && handleSuccess) handleSuccess();
}, [status])
const close = () => {
if (handleError?.onClose) handleError?.onClose();
setOpen(false);
}
const Modal: React.FC = () => {
if (!isOpen || !handleError || !error) return null;
const { fallback } = handleError;
let title: { mainTitle: string, subTitle: string[] } = {
mainTitle: "",
subTitle: [],
}
if (error instanceof NotFoundError) {
title = fallback[404] ?? title;
}
else if (error instanceof BadRequestError) {
title = fallback[400] ?? title;
}
else if (error instanceof UnProcessableError) {
title = fallback[422] ?? title;
}
else throw error;
return (
<div>
<div>
<div>
<p>{title.mainTitle}</p>
<div>
{title.subTitle.map((_subtitle, index) =>
<p>{_subtitle}</p>)}
</div>
</div>
<button onClick={close}>
확인
</button>
</div >
</div >
)
}
return [Modal, result];
}- useMutationError와 동일하게 동작하나, onSuccess를 입력받아 GET 요청에 성공한 경우 동작을 지정할 수 있습니다.
- 어느 페이지에서든 에러 발생으로 인해 에러바운더리가 보이게 된 경우, 이전 페이지로 되돌아가도 화면에 보이는 문제가 존재하여 key로 현재 라우터의 위치를 할당하여 문제를 해결할 수 있었습니다.
- 🚏 완벽한 길을 그리기 위한 노력
- 🪖 버그데이 UT 결과 리포트
- 🐜 어드민 페이지
- 🌊 1차 자체 QA
- 🌊 2차 자체 QA
- 🌊 3차 자체 QA
- 🌊 4차 외부 QA
- 🌊 5차 외부 QA
- ☁️ FE의 GCP를 활용한 배포 방식 및 내부 아키텍쳐
- 🍀 UNIRO의 자연스러운 로딩 화면, 어떤 원리일까? (Suspense)
- 🧪 완벽한(?) 페이지를 위한 LightHouse 점수 개선기
- 🌎 구글 구면 좌표계 도입 여부
⚠️ API 통신 에러 처리- 🥷 바텀시트 만들기
- 💨 최적화 : 효율적인 길 렌더링(Event Capturing)
- 📀 최적화 : 오브젝트 캐싱
- 😎 최적화 : 모든 길 조회 SSE 적용

