네트워크는 언제든 실패합니다. API 오류 상태를 전제로 메시지, 버튼, 로그를 설계해야 사용자가 혼란을 느끼지 않습니다. 이 장의 핵심은 “오류 상태 메시지 + 재시도 버튼 + 백오프 순서”이며, 온라인/offline 이벤트는 선택 확장입니다.
이번 글에서 새로 나오는 용어
- 오류 상태: 데이터 요청이 실패했을 때 보여 주는 전용 화면이나 메시지 영역입니다.
- 재시도 UX: 실패 이후 어떤 간격으로 다시 시도할지, 버튼은 어디에 둘지 설계한 흐름입니다.
- 백오프: 실패할수록 대기 시간을 점점 늘려 서버 폭주를 막는 지연 전략입니다.
- role="alert": 스크린 리더가 즉시 읽어야 할 경고 영역에 붙이는 속성입니다.
- online/offline 이벤트: 브라우저가 네트워크 연결 변화를 감지해 주는 이벤트로, 연결 상태에 따라 UI를 바꾸는 데 사용합니다.
핵심 개념
- 오류 상태(Error State, 실패 화면): 데이터가 없거나 요청이 실패했을 때 보여 주는 화면입니다. 메시지, 아이콘, 재시도 버튼이 포함됩니다.
- 재시도 UX(Retry UX, 다시 시도 흐름): 사용자가 실패 뒤 다시 시도하도록 돕는 절차입니다. 지연 시간, 백오프(Backoff, 실패할수록 대기 시간을 늘리는 전략), 재시도 횟수를 명시합니다.
- 오류 카테고리(Error Category, 유형 분류): 네트워크 연결 문제, 서버 오류, 사용자 입력 오류처럼 유형을 나눠 다른 메시지를 보여 줍니다.
- 로깅과 토스트(Logging & Toast, 기록 + 짧은 알림): 오류를 콘솔이나 모니터링 도구로 보내고, 사용자에게는 짧은 토스트 메시지로 알립니다.
D2로 보는 오류 상태 흐름
이 흐름을 말로 풀면 “사용자 요청 → API 실패 → UI가 메시지를 보여 주고 로깅 → 사용자가 재시도 클릭 → 다시 API 요청”입니다. 즉, 실패 시점마다 UI가 해야 할 일을 미리 정해 두는 셈입니다.
코드로 확인하기
먼저 재시도와 백오프가 결합된 요청 함수를 만듭니다. 아래에 들어가기 전, 가장 단순한 상태 표시를 잠깐 살펴봅니다.
setStatus("불러오는 중");
showRetry(false);
// 실패하면 setStatus("잠시 후 다시 시도") 호출
➡️ 결국 모든 복잡한 흐름도 “상태 텍스트 + 버튼 표시 여부” 두 가지에 기댄다는 점을 기억해 둡니다.
const statusBox = document.querySelector(".status-box");
const retryButton = document.querySelector(".retry-button");
function setStatus(message) {
if (statusBox) statusBox.textContent = message;
}
function showRetry(show) {
if (retryButton) retryButton.classList.toggle("is-hidden", !show);
}
async function fetchTasks({ retries = 3, delayMs = 500 } = {}) {
let attempt = 0;
while (attempt < retries) {
try {
setStatus(`불러오는 중... (시도 ${attempt + 1})`);
const response = await fetch("/api/tasks");
if (!response.ok) throw new Error(`서버 오류 ${response.status}`);
const data = await response.json();
setStatus("완료");
return data;
} catch (error) {
attempt += 1;
console.error("API 실패", error);
if (attempt === retries) {
setStatus("네트워크가 불안정합니다. 잠시 후 다시 시도하세요.");
showRetry(true);
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
}
}
}
retryButton?.addEventListener("click", () => {
showRetry(false);
fetchTasks();
});
백오프를 적용하면 재시도 간격이 늘어나 서버 부담을 줄입니다. “재시도 번호가 커질수록 delayMs * attempt 시간이 늘어난다”만 이해하면 충분합니다.
다음으로 오류 유형별 카드를 렌더링해 상황에 맞는 메시지를 보여 줍니다.
function showErrorCard({ title, description, action }) {
return `
<div class="error-card" role="alert">
<h3>${title}</h3>
<p>${description}</p>
<button data-action="${action.id}">${action.label}</button>
</div>
`;
}
const errorRoot = document.querySelector("#error-root");
function renderError(type) {
const map = {
offline: {
title: "오프라인 상태",
description: "인터넷 연결을 확인한 뒤 다시 시도하세요.",
action: { id: "reload", label: "새로고침" },
},
server: {
title: "서버 응답 지연",
description: "잠시 후 다시 시도하거나 관리자에게 문의하세요.",
action: { id: "retry", label: "재시도" },
},
};
if (errorRoot) errorRoot.innerHTML = showErrorCard(map[type]);
}
errorRoot?.addEventListener("click", (event) => {
const button = event.target;
if (!(button instanceof HTMLButtonElement)) return;
if (button.dataset.action === "reload") location.reload();
if (button.dataset.action === "retry") fetchTasks();
});
오류 유형별 메시지를 미리 정의해 두면 서버에서 받은 에러 코드를 빠르게 대응할 수 있습니다. 여기서 offline, server는 핵심이고, 추가적인 유형은 자유롭게 확장하세요.
마지막으로 브라우저 온라인 상태를 감지해 자동으로 UI를 전환합니다.
window.addEventListener("offline", () => {
renderError("offline");
});
window.addEventListener("online", () => {
setStatus("다시 연결되었습니다.");
fetchTasks();
});
브라우저의 online/offline 이벤트를 사용해 네트워크 상태에 따라 UI를 바꿀 수 있습니다. 이 부분은 선택 확장으로, 기본 재시도 UX가 만들어진 뒤 붙여도 늦지 않습니다.
BrowserMock로 오류 상태 리허설
재시도, 메시지, 토스트가 올바르게 보이는지 확인하려면 상태 전환을 먼저 화면으로 확인하는 편이 좋습니다.
💬 댓글
이 글에 대한 의견을 남겨주세요