FEInterview Prep

Tistory

장애허용성

시스템 일부가 실패해도 전체가 죽지 않게 하는 설계 원칙. FE에서는 Retry·Circuit Breaker·Fallback UI·ErrorBoundary로 장애를 격리한다.

2026-02-13·6분 읽기
아키텍처ReactJavaScript
원문 보기 ↗

핵심 요약

장애허용성은 시스템의 일부가 실패해도 전체가 중단되지 않도록 실패를 격리하고 복구하는 설계 원칙이다. 프런트엔드 관점의 핵심 패턴은 네 가지다: (1) 일시적 오류에 Retry with Exponential Backoff + Jitter, (2) 반복 실패 시 요청을 끊는 Circuit Breaker, (3) 실패해도 화면은 살리는 Fallback UI, (4) 렌더 오류를 격리하는 React ErrorBoundary. 판단 기준은 오류의 성격(transient vs permanent)과 비용(재시도 시 서버 증폭 여부)이다. 무조건 재시도는 오히려 장애를 증폭시키므로 5xx·네트워크 타임아웃만 재시도하고, 4xx는 즉시 실패로 분리해야 한다.

장애허용성(Fault Tolerance)은 "실패는 일어난다"를 전제로 한 설계 사고방식이다. 질문을 뒤집어서 읽자: "이 요청이 실패하면 화면은 어떻게 되는가?" 답이 "전체가 깨진다"면 경계(boundary)와 폴백(fallback)이 부족한 것이다. 프런트엔드에서는 네트워크·서버·런타임 3층에서 실패가 오므로, 각 층마다 격리 단위(try/catch, ErrorBoundary, Suspense boundary)를 세우고 복구 전략(Retry, Fallback, Circuit Breaker)을 붙이는 구조로 읽으면 된다.

서버가 5초 느려지면 전체 페이지가 5초 멈추는 앱과, 해당 위젯만 스켈레톤으로 바뀌는 앱은 같은 장애에서 다른 사용자 경험을 만든다. 면접에서도 단골 주제다:

  • API 실패 시 UI 전략을 묻는 질문은 시니어 FE 포지션 대부분에서 나온다
  • 토스·당근 등 트래픽이 큰 회사일수록 부분 실패 설계를 평가 기준으로 본다
  • "재시도를 몇 번 하세요?" 같은 질문에 숫자만 답하면 감점, Exponential Backoff + Jitter + 중단 조건까지 말해야 가점
  • ErrorBoundary·Suspense·AbortController 는 React 팀이 공식 권장하는 프런트 장애허용성 기본기

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

일시적 vs 영구적 오류

재시도(Retry)는 일시적(transient) 오류에만 해야 한다. 영구적 오류를 재시도하면 서버 부하만 늘리고 사용자 대기 시간도 길어진다.

분류예시재시도?
Transient504, 503, network timeout, ECONNRESETO
Permanent400, 401, 403, 404, 422X (즉시 실패)
Ambiguous429 Too Many RequestsRetry-After 헤더 준수
function isRetryable(err: AxiosError) {
  if (!err.response) return true; // network error
  const s = err.response.status;
  return s >= 500 || s === 408 || s === 429;
}
Transient ErrorIdempotencyRetry-After5xx4xx
자주 하는 오해

모든 실패에 재시도를 거는 것. POST /orders 같은 비멱등(non-idempotent) 요청을 무조건 재시도하면 주문이 2건 생긴다. 재시도는 GET이나 Idempotency-Key가 있는 요청에만.

Exponential Backoff + Jitter

고정 간격 재시도는 서버가 다시 살아나는 순간 모든 클라이언트가 동시에 몰려 장애를 재발시킨다(thundering herd). 지수적으로 간격을 늘리고(Backoff) 난수 지연(Jitter)을 섞어야 한다.

전략1·2·3회차 대기문제
고정1s, 1s, 1s동시 재몰림
Exponential1s, 2s, 4s여전히 동기화됨
Exponential + Jitter~1s, ~2.3s, ~3.7s분산됨
async function retry<T>(fn: () => Promise<T>, max = 3) {
  for (let i = 0; i < max; i++) {
    try { return await fn(); }
    catch (e) {
      if (i === max - 1 || !isRetryable(e)) throw e;
      const base = 2 ** i * 1000;
      const jitter = Math.random() * base;
      await new Promise(r => setTimeout(r, base + jitter));
    }
  }
  throw new Error('unreachable');
}
Exponential BackoffJitterThundering HerdAxios Interceptor
자주 하는 오해

setTimeout(retry, 1000)만 걸고 끝. 최대 재시도 횟수와 최대 총 지연 시간을 함께 정하지 않으면 사용자가 20초 이상 로딩 스피너를 보게 된다.

Circuit Breaker 세 상태

서버가 내려간 것을 감지했는데도 계속 요청하면 사용자 대기 + 서버 회복 지연이라는 이중 손해를 본다. Circuit Breaker 는 일정 실패가 쌓이면 회로를 열어(Open) 일정 시간 동안 요청 자체를 보내지 않고 즉시 폴백으로 전환한다.

상태동작전환 조건
Closed정상 요청 전송실패 임계치 초과 → Open
Open호출 차단, 즉시 fallback쿨다운 경과 → Half-Open
Half-Open샘플 요청 1건만 허용성공 → Closed / 실패 → Open
class Breaker {
  private failures = 0;
  private openedAt = 0;
  constructor(private threshold = 5, private cooldown = 10_000) {}
  async call<T>(fn: () => Promise<T>, fallback: () => T) {
    if (this.state() === 'OPEN') return fallback();
    try {
      const r = await fn();
      this.failures = 0;
      return r;
    } catch (e) {
      if (++this.failures >= this.threshold) this.openedAt = Date.now();
      return fallback();
    }
  }
  private state() {
    if (!this.openedAt) return 'CLOSED';
    return Date.now() - this.openedAt > this.cooldown ? 'HALF_OPEN' : 'OPEN';
  }
}
Circuit BreakerClosed/Open/Half-OpenCooldownBulkhead
자주 하는 오해

Retry 로만 해결하려는 것. 재시도는 한 요청의 복구, Circuit Breaker 는 엔드포인트 전체의 빠른 포기. 둘은 보완 관계지 대체재가 아니다.

React ErrorBoundary + Fallback UI

렌더 중 던져진 예외는 React 16+ 부터 가장 가까운 ErrorBoundary 에서만 잡힌다. 페이지 루트에만 두면 한 컴포넌트 오류로 전체 화면이 흰 페이지가 된다. 위젯 단위로 경계를 쪼개야 부분 실패가 가능하다.

<Page>
  <ErrorBoundary fallback={<HeroSkeleton />}>
    <Hero />
  </ErrorBoundary>
  <ErrorBoundary fallback={<p>추천을 불러오지 못했어요.</p>}>
    <Recommendations />
  </ErrorBoundary>
</Page>

주의: ErrorBoundary렌더·lifecycle·생성자의 동기 오류만 잡는다. 다음은 스스로 처리해야 한다:

  • 이벤트 핸들러 (onClick 내부 throw)
  • 비동기 코드 (setTimeout, fetch().then)
  • SSR 중 서버 측 오류 → Next.js error.tsx
  • Promise rejection → react-error-boundaryuseErrorBoundary().showBoundary(e)
ErrorBoundarygetDerivedStateFromErrorcomponentDidCatchSuspense fallback
자주 하는 오해

함수형 ErrorBoundary 를 직접 만들려 하는 것. 공식적으로는 클래스 컴포넌트만 경계가 될 수 있다. 함수 API 가 필요하면 react-error-boundary 라이브러리를 쓰거나, 로직은 Hook, 경계는 클래스 래퍼로 분리.

AbortController 로 요청 취소

페이지 이탈·타이핑 중인 자동완성처럼 결과가 이미 쓸모없어진 요청은 취소해야 좀비 응답으로 setState 경고와 UI 깜빡임이 안 생긴다. fetchaxios 모두 AbortController 를 지원한다.

useEffect(() => {
  const ac = new AbortController();
  fetch('/api/search?q=' + q, { signal: ac.signal })
    .then(r => r.json())
    .then(setResult)
    .catch(e => { if (e.name !== 'AbortError') setError(e); });
  return () => ac.abort();
}, [q]);

타임아웃도 같은 방식으로: setTimeout(() => ac.abort(), 3000). 사용자에게 무한 로딩 대신 빠른 실패 + Retry 버튼을 주는 것이 장애허용 UX다.

AbortControllerAbortSignalTimeoutStale Response
자주 하는 오해

AbortError 를 일반 에러와 같이 취급해 토스트를 띄우는 것. 의도된 취소는 UI 에러가 아니라 무시 대상이다.

읽는 순서

  1. 1이론

    네 가지 패턴(Retry/Circuit Breaker/Fallback UI/ErrorBoundary) 각각의 목적커버하는 장애 층을 한 줄로 정리한다. transient vs permanent 오류 구분 표도 외울 것.

  2. 2구현

    axios 인터셉터에 Exponential Backoff + Jitter 재시도 로직을 붙이고, AbortController 로 5초 타임아웃을 건다. 그 위에 5회 연속 실패 시 10초간 차단하는 최소 Circuit Breaker 를 얹어 직접 실패를 시뮬레이션해 본다.

  3. 3실무

    지금 맡은 프로젝트에서 ErrorBoundary 가 페이지 루트 한 곳에만 걸려 있지는 않은지, 5xx 응답이 올 때 어떤 UI 가 보이는지, POST 요청에 Idempotency-Key 가 있는지 체크리스트로 점검한다.

  4. 4설명

    동료에게 "주문 API 가 가끔 5초씩 느려지는데 어떻게 개선할까요?"를 5분 안에 설명한다. 타임아웃 → 재시도(멱등·Backoff) → 실패 시 부분 Fallback UI → 반복 실패 시 Circuit Breaker 로 빠른 실패 순서로 이어가면 된다.

면접 연결 질문

medium프런트엔드에서 API 호출 실패를 처리하기 위한 전략에는 어떤 것들이 있고, 어떻게 조합하시겠어요?
힌트

[감점 답변] "try/catch 로 감싸고 토스트 띄워요" 수준. 실패 종류를 구분하지 않음. [좋은 답변] 네 가지 층으로 나눠 답변: (1) 네트워크/전송 층AbortController 타임아웃, (2) 요청 단위Retry with Exponential Backoff + Jitter (5xx·timeout 만, GET/멱등 요청만), (3) 엔드포인트 단위Circuit Breaker 로 연속 실패 시 빠른 포기, (4) 렌더 층ErrorBoundary + 위젯별 Fallback UI. 트레이드오프로 Retry 는 서버 부하 증폭 위험, Circuit Breaker 는 오탐 시 정상 사용자 차단을 함께 언급.

mediumReact `ErrorBoundary` 로 잡을 수 없는 에러는 무엇이고, 그건 어떻게 처리하나요?
힌트

[감점 답변] "비동기는 못 잡는다"까지만 말하고 끝. [좋은 답변] 못 잡는 것 4가지를 구체화: 이벤트 핸들러, setTimeout 등 비동기 콜백, Promise rejection, SSR 서버 오류, ErrorBoundary 자기 자신의 렌더 오류. 대응: 이벤트는 try/catch, 비동기는 React Query 같은 데이터 라이브러리의 onErroruseErrorBoundary().showBoundary(e) 로 렌더 경계로 끌어올리기, Next 13+ 는 error.tsx. 또한 ErrorBoundary클래스 컴포넌트만 가능하다는 점도 언급.

hard재시도(`Retry`) 를 무조건 거는 건 왜 위험하고, 언제 해야 하나요?
힌트

[감점 답변] "3번까지 재시도하면 돼요"로 끝. [좋은 답변] 세 가지 함정을 지적: (1) 비멱등 요청 (POST /orders) 재시도는 중복 처리 — Idempotency-Key 헤더로 방어, (2) 영구 오류(400·404) 재시도는 서버 부하만 증폭 — 5xx·네트워크 오류만 재시도, (3) 동기화된 재시도 는 thundering herd 유발 — Exponential Backoff + Jitter 필요. 추가로 429Retry-After 헤더 존중, 최대 총 지연 시간을 설정해 UX 보호.

hardCircuit Breaker 패턴을 프런트엔드에서 구현한다면 어떻게 접근하시겠어요?
힌트

[감점 답변] 세 상태 이름만 나열. [좋은 답변] Closed/Open/Half-Open전환 조건과 함께 설명: 실패 임계치 초과 시 Open, 쿨다운 경과 후 Half-Open 에서 샘플 요청으로 복구 테스트. 프런트 특유 고려 사항도 덧붙이기: 엔드포인트별 인스턴스(전역 1개면 다른 API까지 막힘), 메모리에만 저장(탭 새로고침 시 초기화), 서버리스/CDN 이 앞단에 있으면 같은 장애가 재현 안 될 수 있어 서버 사이드 Circuit Breaker 와 역할 분리.

자기 점검

`GET /search` 와 `POST /orders` 중 재시도를 걸어도 안전한 것은 무엇이고 왜 그런가?
멱등idempotentIdempotency-Key중복 주문
자주 하는 오해

"POST 도 실패하면 어차피 실패니까 재시도해도 된다"는 오해. 네트워크 단절은 요청이 도달 후 응답만 실패한 경우가 있어 서버에서는 이미 처리된 상태일 수 있다. Idempotency-Key 없이는 중복 생성이 일어난다.

Retry 와 Circuit Breaker 를 둘 다 쓰는 이유는? 한쪽만 쓰면 어떤 문제가 생기나?
Exponential Backoffthundering herd빠른 실패fail fast
자주 하는 오해

"Retry 만 있으면 충분"이라는 생각. 서버가 완전히 내려간 상황에서 모든 클라이언트가 몇 초마다 재시도를 반복하면 서버 복구 자체가 지연된다. Circuit Breaker 는 그 트래픽을 끊어 주는 역할.

`ErrorBoundary` 를 페이지 루트에만 두면 어떤 UX 문제가 생기나?
부분 실패위젯fallback격리
자주 하는 오해

"경계는 하나만 있으면 된다"는 생각. 한 작은 위젯의 렌더 오류로 페이지 전체가 흰 화면이 되면 가용성이 오히려 0이 된다. 위젯 단위로 경계를 쪼개 다른 영역은 계속 동작하게 해야 한다.