[JavaScript 시리즈 15편] API 오류 상태와 재시도 UX 설계

English version

네트워크는 언제든 실패합니다. API 오류 상태를 전제로 메시지, 버튼, 로그를 설계해야 사용자가 혼란을 느끼지 않습니다. 이 장의 핵심은 “오류 상태 메시지 + 재시도 버튼 + 백오프 순서”이며, 온라인/offline 이벤트는 선택 확장입니다.

이번 글에서 새로 나오는 용어

  1. 오류 상태: 데이터 요청이 실패했을 때 보여 주는 전용 화면이나 메시지 영역입니다.
  2. 재시도 UX: 실패 이후 어떤 간격으로 다시 시도할지, 버튼은 어디에 둘지 설계한 흐름입니다.
  3. 백오프: 실패할수록 대기 시간을 점점 늘려 서버 폭주를 막는 지연 전략입니다.
  4. role="alert": 스크린 리더가 즉시 읽어야 할 경고 영역에 붙이는 속성입니다.
  5. online/offline 이벤트: 브라우저가 네트워크 연결 변화를 감지해 주는 이벤트로, 연결 상태에 따라 UI를 바꾸는 데 사용합니다.

핵심 개념

  • 오류 상태(Error State, 실패 화면): 데이터가 없거나 요청이 실패했을 때 보여 주는 화면입니다. 메시지, 아이콘, 재시도 버튼이 포함됩니다.
  • 재시도 UX(Retry UX, 다시 시도 흐름): 사용자가 실패 뒤 다시 시도하도록 돕는 절차입니다. 지연 시간, 백오프(Backoff, 실패할수록 대기 시간을 늘리는 전략), 재시도 횟수를 명시합니다.
  • 오류 카테고리(Error Category, 유형 분류): 네트워크 연결 문제, 서버 오류, 사용자 입력 오류처럼 유형을 나눠 다른 메시지를 보여 줍니다.
  • 로깅과 토스트(Logging & Toast, 기록 + 짧은 알림): 오류를 콘솔이나 모니터링 도구로 보내고, 사용자에게는 짧은 토스트 메시지로 알립니다.

D2로 보는 오류 상태 흐름

사용자UI 상태 패널재시도 버튼API 서버로깅 서비스 데이터 요청fetch실패 응답버튼 노출오류 기록클릭재시도

이 흐름을 말로 풀면 “사용자 요청 → 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로 오류 상태 리허설

재시도, 메시지, 토스트가 올바르게 보이는지 확인하려면 상태 전환을 먼저 화면으로 확인하는 편이 좋습니다.

http://localhost:5173/tasks
DEGRADED

UI Preview

오류 상태도 제품의 일부로 설계하기

성공 화면만 만드는 것이 아니라, 실패했을 때 어떤 문구와 버튼이 보여야 하는지도 함께 설계해야 합니다.

retry UXbackoffrole=alert

Loading

첫 요청

상태 박스에 불러오는 중 메시지 표시.

Error

서버 지연

role=alert 카드와 재시도 버튼 노출.

Recovered

재시도 성공

목록이 다시 보이고 상태 박스는 완료로 전환.

  • 확인할 점: 실패했을 때 버튼과 메시지가 동시에 보이는지
  • 확인할 점: 성공으로 돌아오면 에러 카드가 그대로 남지 않는지
  • 확인할 점: 사용자가 다음 행동을 쉽게 고를 수 있는지

왜 중요한가

  • 대부분의 사용자 불만은 “왜 실패했는지 몰랐다”는 데서 나옵니다. 명확한 에러 메시지는 신뢰를 지켜 줍니다.
  • 재시도 버튼과 백오프는 서버 비용을 줄이고, 문제 발생 시에도 앱이 즉시 회복할 수 있게 합니다.
  • 오류 유형을 구분하면 모니터링이나 알림 시스템을 구축할 때도 큰 도움이 됩니다.

실습

  • 핵심 따라 하기: fetchTasks와 같은 재시도 로직을 작성해 로딩/성공/실패 메시지를 모두 표시합니다.
  • 선택 확장하기: 오류 카드를 컴포넌트화하고 offline, server, validation 유형별 메시지를 정의합니다.
  • 디버깅: 네트워크 탭에서 Offline 모드를 켜거나 잘못된 URL을 넣어 실패를 재현하고, 콘솔과 UI에 각각 어떤 메시지가 찍히는지 기록합니다.
  • 완료 기준: 실패 → 메시지 → 재시도 버튼 → 성공 흐름을 3회 이상 반복해도 화면이 깨지지 않으면 실습이 끝납니다.

마무리

API는 언제든 실패합니다. 오류 상태와 재시도 UX를 기본값으로 삼으면 사용자가 믿고 기다릴 수 있는 제품을 만들 수 있습니다. 다음 글에서는 라우팅과 페이지 이동 구조를 살펴볼 예정입니다.

💬 댓글

이 글에 대한 의견을 남겨주세요