[우테코] Level5 네트워크 에러 잡기
오프라인 상태입니다. 네트워크를 확인해주세요.
관련 PR: Errorboundary 전략 세우기, Network에 따른 UI 변경
🔥1. Toast 여러 개 띄우기
네트워크 오류일 때는 토스트 계속 띄우기 관련 커밋
저희 서비스는 오프라인일 때 이용할 수 있는 서비스가 없기 때문에 사용자의 빠른 네트워크 연결을 유도해야 합니다.
이러한 이유로 아래와 같이 리팩토링을 하였습니다.
1. showToast 함수에 type 추가
[이전]
- useToast 훅을 선언할 때 type(
error
,success
)을 prop으로 넘겨 주었습니다. - 하나의 컴포넌트 안에서 네트워크 상태에 따라 ‘오프라인 시 에러’, 온라인 시 성공’ 토스트를 모두 써야 하는데, 훅 생성 시점에 한 가지만 고정하도록 되어 있었습니다.
const { showToast } = useToast("error");
[이후]
- 훅 선언부가 아니라 showToast 함수를 호출할 때마다 type을 인자로 넘기도록 옮겼습니다.
- 이렇게 하면 하나의 컴포넌트 안에서도 상황별로 원하는 타입의 토스트를 모두 띄울 수 있습니다.
const { showToast } = useToast();
showToast("실패했습니다", "error");
showToast("성공했습니다.", "success");
2. showToast 함수에 durationMs 추가
[이전]
durationMs
가 없고 항상 2500ms를 보여주었습니다.
const showToast = (message: string, type: ToastType = "error") => {
setToast({ isOpen: true, message, type });
setTimeout(() => {
setToast((prev) => ({ ...prev, isOpen: false, type }));
}, 2500);
};
[이후]
- 무한으로 보여주는 Toast가 필요하여
showToast
함수에durationMs
인자를 추가하였고, 무한으로 띄우고 싶을 땐Infinity
를 사용하였습니다. (Infinity
도 숫자 타입) durationMs
가 끝났을 때 자동으로 닫히는 건 아래에<Toast />
에서 설명하겠습니다.
useToast.ts
const showToast = (
message: string,
type: ToastType = "error",
durationMs = 2500 // durationMs 기본은 2500, 무한으로 띄우고 싶을 땐 Infinity 사용
) => {
setToasts((prev) => {
return [...prev, { message, type, durationMs }];
});
};
3. useToast 훅에 closeToast 함수 추가
- 무한으로 보여주는 Toast가 생겼기 때문에 닫는 동작도 추가하였습니다.
- 현재 떠있는 toast 중에서 props의 message와 동일 toast를 닫습니다.
useToast.ts
const closeToast = (message: string) => {
setToasts((prev) => prev.filter((t) => t.message !== message));
};
사용 - useNetwork.ts
showToast(MESSAGES.ERROR.OFFLINE, "error", Infinity); // 오프라인 에러 토스트 열기
closeToast(MESSAGES.ERROR.OFFLINE); // 오프라인 에러 토스트 닫기
4. showToast 함수에 message로 중복 처리
- 사용자들이 같은 버튼을 계속 누르면 동일한 메세지 Toast가 계속 떠서 화면이 안 보이게 됩니다.
message
로 중복 처리를 하여 같은 메세지 Toast는 한 번만 뜨게 합니다.
const showToast = (
message: string,
type: ToastType = "error",
durationMs = 2500
) => {
setToasts((prev) => {
const alreadyExists = prev.some((t) => t.message === message); // 메세지로 중복 제거
if (alreadyExists) return prev;
return [...prev, { message, type, durationMs }];
});
};
5. 토스트 여러 개 띄우기, 자동 닫기
- 위에서 자동으로 닫히는 건 아래
<Toast />
에서 설명한다고 했는데 바로 이 컴포넌트입니다. - Network 에러와 mutation 동작에서 모두 Toast를 띄울 예정이기 때문에 Toast를 여러 개 띄워야 합니다.
- toasts 배열로 받고, durationMs이 지나면 closingToasts에 담아 toast를 제거합니다.
Toast.tsx
const Toast = () => {
const container = document.getElementById("toast");
const toasts = useContext(ToastContext);
const { closeToast } = useToast();
const [closingToasts, setClosingToasts] = useState<string[]>([]);
/**
* toasts 중에서 durationMs가 지난 toast는 closingToasts에 담습니다.
*/
useEffect(() => {
const timers = toasts.map((toast) => {
if (
!closingToasts.includes(toast.message) &&
Number.isFinite(toast.durationMs)
) {
return setTimeout(() => {
setClosingToasts((prev) => [...prev, toast.message]);
}, toast.durationMs);
}
});
return () => {
timers.forEach(clearTimeout);
};
}, [toasts, closingToasts]);
/**
* closingToasts 중 완벽히 화면에서 사라진 toast는 제거합니다.
*/
useEffect(() => {
const timers = closingToasts.map((message) =>
setTimeout(() => {
closeToast(message);
setClosingToasts((prev) => prev.filter((m) => m !== message));
}, 400)
);
return () => {
timers.forEach(clearTimeout);
};
}, [closingToasts, closeToast]);
if (!container) return null;
return createPortal(
<S.ToastContainer>
{toasts.map(({ message, type }) => (
<S.Wrapper
key={message}
$type={type}
$closeAnimation={closingToasts.includes(message)}
role="alert"
aria-live="assertive"
>
{message}
</S.Wrapper>
))}
</S.ToastContainer>,
container
);
};
export default Toast;
🔥2. Network 에러 잡기
[레퍼런스]
✅ 유튜브의 UX가 좋다고 판단하여 참고해서 개발하였습니다.
1. 오프라인 상태로 클릭 event -> get 요청이면 로딩 중 표시 -> 온라인으로 바뀌면 자동 fetching
2. 오프라인 상태로 클릭 event -> 나머지 요청이면 에러 Toast 띄우기
3. 오프라인 상태로 라우팅 -> Fallback UI 보여주기
[문제점]
🤔 navigator.onLine 문제
이전 버전에서는 navigator.onLine
을 사용하여 네트워크 상태를 확인했습니다. 하지만 이는 크롬 기반 브라우저에서는 잘 작동하지 않습니다. 거짓 네거티브와 관련된 많은 문제가 있으며, 이로 인해 쿼리가 오프라인으로 잘못 표시될 수 있었습니다.
탠스택 쿼리에서는 networkMode 속성이 있기 때문에 이를 위한 온라인/오프라인 판별 클래스가 있을 거라고 판단했습니다. 공식문서에서 OnlineManager를 사용한다는 것을 알았고 onlineManager.ts 코드를 참고하여 useNetwork 훅을 만들었습니다.
useNetwork.ts
window 객체의 online
과 offline
이벤트를 사용하여 네트워크를 감지 가능
useEffect(() => {
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline;
1. queries의 networkMode: "online"으로 변경
💡 레퍼런스 1번 해결: get 요청이면 로딩 중 표시 -> 온라인으로 바뀌면 자동 fetching
networkMode “online”: TanStack Query도 재시도 메커니즘을 일시 중지, 일시 중지된 쿼리는 네트워크 연결을 다시 얻으면 계속 실행
-> networkMode를 “online”으로 변경하여 네트워크가 연결되었을 때 자동 fetching을 가능하게 했습니다.
2. Network 오프라인이면 계속 Toast 띄우기
💡 레퍼런스 2번 해결: 오프라인 상태로 클릭 event -> 나머지 요청이면 에러 Toast 띄우기
- 오프라인이면 Toast를 계속 띄우고, 온라인으로 바뀌면 오프라인 Toast를 닫고, 성공 Toast를 보여줍니다.
- post, put, delete 요청일 땐 에러 Toast를 띄웁니다.
useNetwork.ts
// 온라인이면 에러 토스트 닫고, 성공 토스트 띄우기
const handleOnline = () => {
setIsOnline(true);
closeToast(MESSAGES.ERROR.OFFLINE);
showToast(MESSAGES.SUCCESS.ONLINE, "success");
};
// 오프라인이면 에러 토스트 무한으로 띄우기
const handleOffline = () => {
setIsOnline(false);
showToast(MESSAGES.ERROR.OFFLINE, "error", Infinity);
};
3. 오프라인 상태로 페이지 이동하면 Fallback UI 보여주기
💡 레퍼런스 3번 해결: 오프라인 상태로 라우팅 -> Fallback UI 보여주기
전역 ErrorBoundary에서 error를 잡아 !isOnline
, error.name === "ChunkLoadError"
, NetworkError
일 때 새로고침 Fallback UI를 보여줍니다.
ErrorBoundarySwitch.tsx
const ErrorBoundarySwitch = ({
error,
resetError,
}: ErrorBoundarySwitchProps) => {
const isOnline = useNetwork();
const queryClient = useQueryClient();
const handleRetry = () => {
resetError();
queryClient.invalidateQueries({
predicate: (query) => query.state.status === "error",
});
};
switch (true) {
case !isOnline || error.name === "ChunkLoadError":
case error instanceof NetworkError:
return <NetworkFallback onRetry={handleRetry} />; // 네트워크 에러
case error instanceof ApiError:
return <ApiFallback onRetry={handleRetry} errorMessage={error.message} />;
default:
return <DefaultFallback onRetry={handleRetry} />;
}
};
댓글남기기