TypeScript fetch API 에러 핸들링 7단계 — 재시도·타임아웃 실전 패턴

목차

Stack Overflow 2024 Survey 기준, 웹 개발자의 약 70%가 HTTP 클라이언트로 fetch API를 사용한다. 그런데 fetch()는 HTTP 404나 500 응답에서 Promise를 reject하지 않는다. MDN 공식 문서에 명시된 내용이다. TypeScript fetch API 에러 핸들링을 제대로 하지 않으면 response.okfalse인 응답이 성공으로 처리되는 사고가 생긴다.

데이터 파이프라인에서 외부 API를 호출할 때 이 문제가 특히 치명적이다. 하나의 fetch 실패가 전체 ETL 작업을 중단시킬 수 있기 때문. 이 글은 TypeScript fetch API 에러 핸들링에서 다뤄야 할 에러 유형 분류, 타입 안전한 처리 패턴, 타임아웃, 재시도 로직까지 정리한다.

fetch()가 reject하는 조건과 안 하는 조건

fetch의 가장 큰 함정은 HTTP 에러 상태 코드에서 reject하지 않는다는 점이다. axiosky 같은 라이브러리에 익숙하면 이 동작을 간과하기 쉽다.

reject되는 경우 (TypeError)

MDN 문서 기준, fetch()가 Promise를 reject하며 TypeError를 던지는 조건은 다음과 같다:

  • URL이 유효하지 않을 때
  • URL에 credentials(username:password)가 포함되어 있을 때
  • RequestInit 옵션에 잘못된 값이 들어갔을 때
  • 네트워크 연결 자체가 불가능할 때 (오프라인, DNS 실패 등)
  • 브라우저 권한 정책(Permissions Policy)으로 차단됐을 때

reject되지 않는 경우

HTTP 상태 코드가 404, 500, 503이어도 fetch는 정상적으로 Response 객체를 반환한다. reject되지 않는다. 이건 설계상 의도된 동작이다.

// 이 코드는 404에서도 에러를 던지지 않는다
const response = await fetch("https://api.example.com/users/999");
console.log(response.status); // 404
console.log(response.ok);     // false — 이걸 직접 체크해야 한다
fetch()의 가장 흔한 실수
`try/catch`만으로 모든 에러를 잡을 수 있다고 생각하는 것이 가장 위험하다. HTTP 4xx/5xx 에러는 catch 블록에 도달하지 않는다. `response.ok` 또는 `response.status`를 반드시 확인해야 한다.
이 동작을 모르면 데이터 파이프라인에서 404 응답 본문을 정상 데이터로 파싱하려다 JSON 에러가 터지는 2차 사고로 이어진다.

에러 유형 분류 — TypeScript 타입으로 정의하기

TypeScript fetch API 에러 핸들링의 첫 단계는 에러 유형을 명확히 분류하는 것이다. fetch 호출에서 발생할 수 있는 에러는 크게 4가지로 나뉜다.

에러 유형발생 시점Promise reject 여부예시
네트워크 에러연결 자체 실패✅ reject (TypeError)DNS 실패, 오프라인
HTTP 상태 에러서버 응답 수신 후❌ reject 안 됨401, 404, 500, 503
파싱 에러response.json() 호출 시✅ reject (SyntaxError)HTML을 JSON으로 파싱 시도
타임아웃/취소시간 초과 또는 수동 취소✅ reject (DOMException)AbortController.abort()

이걸 TypeScript discriminated union으로 표현하면 이렇게 된다:

interface NetworkError {
  type: "NETWORK_ERROR";
  message: string;
  cause?: unknown;
}

interface HttpError {
  type: "HTTP_ERROR";
  status: number;
  statusText: string;
  body?: unknown;
}

interface ParseError {
  type: "PARSE_ERROR";
  message: string;
  rawText?: string;
}

interface TimeoutError {
  type: "TIMEOUT_ERROR";
  message: string;
  elapsedMs: number;
}

type FetchError = NetworkError | HttpError | ParseError | TimeoutError;

type 필드가 discriminant 역할을 한다. switch문에서 타입 좁힘(narrowing)이 자동으로 동작하기 때문에 각 에러 유형별로 다른 처리 로직을 타입 안전하게 작성할 수 있다.

discriminated union을 쓰는 이유
`Error` 클래스를 상속하는 방법도 있지만, `instanceof` 체크는 번들러 설정이나 모듈 경계에서 깨질 수 있다. 반면 문자열 리터럴 `type` 필드 기반 유니온은 직렬화/역직렬화에도 안전하고 JSON 로깅에도 그대로 쓸 수 있다.
## Result 타입으로 감싸는 타입 안전한 fetch wrapper

try/catch에서 catcherrorunknown 타입이다 (TypeScript 4.4+). 이걸 그대로 쓰면 타입 안전성이 없다. Result 패턴으로 감싸면 에러 처리를 컴파일 타임에 강제할 수 있다.

Result 타입 정의

type Result<T, E = FetchError> =
  | { ok: true; data: T }
  | { ok: false; error: E };

이 패턴은 Rust의 Result<T, E>에서 가져온 것이다. ok 필드를 체크하면 TypeScript 컴파일러가 dataerror를 자동으로 좁힌다.

제네릭 fetch wrapper 구현

async function safeFetch<T>(
  url: string,
  options?: RequestInit
): Promise<Result<T>> {
  let response: Response;

  try {
    response = await fetch(url, options);
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") {
      return {
        ok: false,
        error: {
          type: "TIMEOUT_ERROR",
          message: error.message,
          elapsedMs: 0, // 실제로는 시작 시간과 비교해서 계산
        },
      };
    }
    return {
      ok: false,
      error: {
        type: "NETWORK_ERROR",
        message: error instanceof Error ? error.message : String(error),
        cause: error,
      },
    };
  }

  if (!response.ok) {
    let body: unknown;
    try {
      body = await response.json();
    } catch {
      // JSON 파싱 실패 시 body는 undefined로 유지
    }
    return {
      ok: false,
      error: {
        type: "HTTP_ERROR",
        status: response.status,
        statusText: response.statusText,
        body,
      },
    };
  }

  try {
    const data = (await response.json()) as T;
    return { ok: true, data };
  } catch (error) {
    const rawText = await response.text().catch(() => undefined);
    return {
      ok: false,
      error: {
        type: "PARSE_ERROR",
        message: error instanceof Error ? error.message : "JSON parse failed",
        rawText,
      },
    };
  }
}

호출하는 쪽 코드가 깔끔해진다:

interface User {
  id: number;
  name: string;
  email: string;
}

const result = await safeFetch<User>("https://api.example.com/users/1");

if (!result.ok) {
  switch (result.error.type) {
    case "NETWORK_ERROR":
      console.error("네트워크 문제:", result.error.message);
      break;
    case "HTTP_ERROR":
      console.error(`HTTP ${result.error.status}:`, result.error.statusText);
      break;
    case "PARSE_ERROR":
      console.error("JSON 파싱 실패:", result.error.rawText?.slice(0, 200));
      break;
    case "TIMEOUT_ERROR":
      console.error("타임아웃:", result.error.message);
      break;
  }
  return;
}

// result.data는 여기서 User 타입으로 좁혀진다
console.log(result.data.name);

switch문에서 result.error.type을 분기할 때 각 case 안에서 result.error의 타입이 자동으로 좁혀진다. HTTP_ERROR 분기에서 result.error.status에 접근 가능한 이유가 이것이다.

AbortSignal.timeout()으로 타임아웃 처리

fetch에는 내장 타임아웃 옵션이 없다. 타임아웃을 구현하려면 AbortController를 쓰거나, 더 간단한 방법으로 AbortSignal.timeout()을 쓰면 된다.

AbortSignal.timeout() — 모던 방식

MDN AbortSignal.timeout() 문서 기준, 이 메서드는 2024년 4월부터 주요 브라우저에서 모두 지원된다(Baseline 2024). Node.js는 v17.3+에서 사용 가능하다.

// 5초 타임아웃. 한 줄이면 된다.
const response = await fetch(url, {
  signal: AbortSignal.timeout(5000),
});

타임아웃 시 TimeoutError DOMException이 발생한다. 사용자가 브라우저 중지 버튼을 누르면 AbortError가 발생한다. 에러 이름(error.name)으로 구분하면 된다.

AbortController — 수동 제어가 필요할 때

타임아웃과 수동 취소를 동시에 지원해야 하면 AbortSignal.any()로 신호를 합성한다:

function fetchWithManualCancel(
  url: string,
  timeoutMs: number,
  externalController?: AbortController
): { promise: Promise<Response>; cancel: () => void } {
  const manualController = externalController ?? new AbortController();

  const combinedSignal = AbortSignal.any([
    AbortSignal.timeout(timeoutMs),
    manualController.signal,
  ]);

  return {
    promise: fetch(url, { signal: combinedSignal }),
    cancel: () => manualController.abort(),
  };
}
AbortSignal.any()의 브라우저 지원
`AbortSignal.any()`는 Chrome 116+, Firefox 124+, Safari 17.4+에서 지원된다. Node.js는 v20+. 지원 범위가 넉넉하지만 구형 환경을 지원해야 하면 폴리필이 필요하다.
`safeFetch` wrapper에 타임아웃을 통합하면:
async function safeFetchWithTimeout<T>(
  url: string,
  options?: RequestInit & { timeoutMs?: number }
): Promise<Result<T>> {
  const { timeoutMs = 10000, ...fetchOptions } = options ?? {};

  const startTime = Date.now();

  return safeFetch<T>(url, {
    ...fetchOptions,
    signal: fetchOptions.signal
      ? AbortSignal.any([fetchOptions.signal, AbortSignal.timeout(timeoutMs)])
      : AbortSignal.timeout(timeoutMs),
  }).then((result) => {
    if (!result.ok && result.error.type === "TIMEOUT_ERROR") {
      return {
        ok: false as const,
        error: {
          ...result.error,
          elapsedMs: Date.now() - startTime,
        },
      };
    }
    return result;
  });
}

기본 타임아웃을 10초로 설정했다. 데이터 파이프라인에서 외부 API를 호출할 때 타임아웃 없이 요청을 보내면, 응답이 안 오는 상태로 커넥션이 무한 대기할 수 있다. ETL 작업 전체가 멈추는 원인이 된다.

재시도 패턴 — 지수 백오프와 멱등성

일시적 네트워크 오류나 서버 과부하(503 Service Unavailable)는 재시도로 해결되는 경우가 많다. 단, 무조건 재시도하면 안 된다.

재시도해도 되는 조건

function isRetryable(error: FetchError): boolean {
  switch (error.type) {
    case "NETWORK_ERROR":
      return true; // 네트워크 에러는 항상 재시도
    case "TIMEOUT_ERROR":
      return true; // 타임아웃도 재시도
    case "HTTP_ERROR":
      // 429(Rate Limit), 502, 503, 504만 재시도
      return [429, 502, 503, 504].includes(error.status);
    case "PARSE_ERROR":
      return false; // 파싱 에러는 재시도해도 의미 없음
  }
}
POST/PUT/DELETE 재시도 주의
`GET`은 멱등(idempotent)하니까 재시도해도 부작용이 없다. `POST`를 재시도하면 결제가 중복 처리될 수 있다. 멱등키(Idempotency-Key) 헤더를 지원하는 API가 아니면 `POST` 재시도는 하지 않는 게 안전하다.
### 지수 백오프 + 지터 구현
interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
  retryableMethods?: string[];
}

const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  retryableMethods: ["GET", "HEAD", "OPTIONS"],
};

function calculateDelay(attempt: number, config: RetryConfig): number {
  // 지수 백오프: 1초 → 2초 → 4초 → 8초 ...
  const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
  // 최대 지연 시간 제한
  const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
  // 지터(jitter) 추가: 0.5배 ~ 1.5배 범위에서 랜덤화
  const jitter = 0.5 + Math.random();
  return Math.floor(cappedDelay * jitter);
}

지수 백오프에 지터를 추가하는 이유는 thundering herd 문제를 피하기 위해서다. 서버가 다운됐다 복구된 직후, 대기하던 클라이언트가 동시에 재시도하면 서버가 다시 죽는다. 지터가 재시도 시점을 분산시킨다.

전체 재시도 wrapper

async function fetchWithRetry<T>(
  url: string,
  options?: RequestInit & { timeoutMs?: number; retry?: Partial<RetryConfig> }
): Promise<Result<T>> {
  const config = { ...DEFAULT_RETRY_CONFIG, ...options?.retry };
  const method = options?.method?.toUpperCase() ?? "GET";

  // 멱등하지 않은 메서드는 재시도하지 않음
  if (!config.retryableMethods?.includes(method)) {
    return safeFetchWithTimeout<T>(url, options);
  }

  let lastResult: Result<T> | null = null;

  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    if (attempt > 0) {
      const delay = calculateDelay(attempt - 1, config);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }

    const result = await safeFetchWithTimeout<T>(url, options);

    if (result.ok) {
      return result;
    }

    lastResult = result;

    if (!isRetryable(result.error)) {
      return result; // 재시도 불가능한 에러는 즉시 반환
    }

    // 429 응답에 Retry-After 헤더가 있으면 해당 시간만큼 대기
    if (
      result.error.type === "HTTP_ERROR" &&
      result.error.status === 429
    ) {
      // Retry-After 처리는 별도 로직 필요 (아래 메모 참조)
    }
  }

  return lastResult!;
}

이 코드를 쓸 때 하나 주의할 점. for 루프 안에서 await으로 대기하는 건 단순하지만, 전체 타임아웃 관리가 없다. 개별 요청 타임아웃(timeoutMs)과 전체 재시도 타임아웃을 분리하고 싶으면 최상위에 AbortSignal.timeout()을 하나 더 걸어야 한다.

TypeScript fetch API 에러 핸들링 흐름 정리

전체 에러 처리 흐름을 다이어그램으로 보면 이렇다:

flowchart TD
    A["fetch(url, options)"] --> B{Promise reject?}
    B -->|Yes| C{error.name?}
    C -->|TimeoutError| D["TIMEOUT_ERROR"]
    C -->|AbortError| E["수동 취소"]
    C -->|TypeError| F["NETWORK_ERROR"]
    B -->|No| G{response.ok?}
    G -->|false| H["HTTP_ERROR<br/>status, statusText"]
    G -->|true| I{response.json()}
    I -->|SyntaxError| J["PARSE_ERROR"]
    I -->|성공| K["Result: ok=true"]
    D --> L{재시도 가능?}
    F --> L
    H --> L
    J --> M["즉시 실패 반환"]
    L -->|Yes| N["지수 백오프 대기"]
    L -->|No| M
    N --> A

TypeScript fetch API 에러 핸들링에서 핵심은 이 분기 구조를 코드로 그대로 옮기는 것이다. try/catch로 네트워크/타임아웃 에러를 잡고, response.ok로 HTTP 에러를 체크하고, response.json()의 실패까지 처리하면 누락되는 케이스가 없다.

429 Rate Limit 대응과 Retry-After 헤더

API를 대량 호출하는 데이터 파이프라인에서 가장 자주 마주치는 에러가 429 Too Many Requests다. 이건 단순 재시도가 아니라 서버가 알려주는 대기 시간을 존중해야 한다.

Retry-After 파싱

Retry-After 헤더는 두 가지 포맷이 가능하다:

  • 초 단위 숫자: Retry-After: 120 (120초 후 재시도)
  • HTTP 날짜: Retry-After: Fri, 31 Dec 2027 23:59:59 GMT
function parseRetryAfter(headerValue: string | null): number | null {
  if (!headerValue) return null;

  // 숫자인 경우 (초 단위)
  const seconds = Number(headerValue);
  if (!Number.isNaN(seconds) && seconds >= 0) {
    return seconds * 1000; // ms로 변환
  }

  // HTTP 날짜인 경우
  const date = new Date(headerValue);
  if (!Number.isNaN(date.getTime())) {
    const delayMs = date.getTime() - Date.now();
    return Math.max(0, delayMs);
  }

  return null;
}

재시도 로직에 통합할 때는 calculateDelay 결과와 Retry-After 값 중 더 큰 쪽을 택한다. 서버가 “60초 기다려”라고 했는데 2초 만에 재시도하면 또 429가 돌아온다.

Retry-After 헤더의 현실
모든 API가 Retry-After를 보내주진 않는다. 보내더라도 초 단위 숫자 포맷이 대부분이다. HTTP 날짜 포맷은 드물지만 스펙상 지원해야 한다. [MDN Retry-After 문서](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)를 참고.
## 실전 패턴 — 데이터 파이프라인용 fetch 클라이언트

지금까지 만든 조각들을 합치면 하나의 fetch 클라이언트가 된다. 데이터 파이프라인에서 외부 API를 호출할 때 필요한 요구사항을 정리하면:

  • 모든 요청에 기본 타임아웃 적용
  • 재시도 가능한 에러만 자동 재시도
  • 요청/응답 로깅 (디버깅용)
  • 타입 안전한 응답

클라이언트 인터페이스

interface FetchClientConfig {
  baseUrl: string;
  defaultTimeoutMs: number;
  retry: RetryConfig;
  headers?: Record<string, string>;
  onRequest?: (url: string, init: RequestInit) => void;
  onResponse?: (url: string, result: Result<unknown>) => void;
}

class TypedFetchClient {
  constructor(private config: FetchClientConfig) {}

  async get<T>(path: string, options?: RequestInit): Promise<Result<T>> {
    const url = `${this.config.baseUrl}${path}`;
    const init: RequestInit = {
      ...options,
      method: "GET",
      headers: {
        ...this.config.headers,
        ...options?.headers,
      },
    };

    this.config.onRequest?.(url, init);

    const result = await fetchWithRetry<T>(url, {
      ...init,
      timeoutMs: this.config.defaultTimeoutMs,
      retry: this.config.retry,
    });

    this.config.onResponse?.(url, result);

    return result;
  }

  async post<T>(
    path: string,
    body: unknown,
    options?: RequestInit
  ): Promise<Result<T>> {
    const url = `${this.config.baseUrl}${path}`;
    const init: RequestInit = {
      ...options,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...this.config.headers,
        ...options?.headers,
      },
      body: JSON.stringify(body),
    };

    this.config.onRequest?.(url, init);

    // POST는 재시도하지 않음 (멱등키 없는 한)
    const result = await safeFetchWithTimeout<T>(url, {
      ...init,
      timeoutMs: this.config.defaultTimeoutMs,
    });

    this.config.onResponse?.(url, result);

    return result;
  }
}

사용 예시

const apiClient = new TypedFetchClient({
  baseUrl: "https://api.example.com",
  defaultTimeoutMs: 10000,
  retry: { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000 },
  headers: { Authorization: "Bearer token-here" },
  onResponse: (url, result) => {
    if (!result.ok) {
      console.error(`[API Error] ${url}:`, result.error);
    }
  },
});

// 타입이 자동으로 추론된다
const users = await apiClient.get<User[]>("/users");
if (users.ok) {
  users.data.forEach((u) => console.log(u.name));
}

이 구조에서 onRequest/onResponse 콜백은 로깅, 메트릭 수집, 알림 연동 등에 쓸 수 있다. 데이터 파이프라인이라면 실패한 요청의 URL과 에러 타입을 구조화 로그로 남기는 게 나중에 디버깅할 때 훨씬 편하다.

fetch vs axios vs ky — 에러 핸들링 관점 비교

라이브러리 선택지를 에러 핸들링 관점에서 비교한다.

기능fetch (native)axios 1.xky 1.x
HTTP 에러 자동 throw❌ 수동 체크 필요✅ 자동✅ 자동
타임아웃 내장❌ AbortSignal 필요timeout 옵션timeout 옵션
재시도 내장❌ 직접 구현❌ 직접 구현retry 옵션
인터셉터❌ 직접 구현✅ interceptors✅ hooks
번들 사이즈0 KB (내장)~13 KB (min+gzip)~3.5 KB (min+gzip)
Node.js 지원v18+ 내장✅ (내부에서 fetch 사용)
TypeScript 타입lib.dom.d.ts 포함자체 타입 번들자체 타입 번들

네이티브 fetch를 쓸지, 라이브러리를 쓸지는 프로젝트 상황에 달려 있다.

fetch를 직접 쓰면 의존성이 0개다. Node.js v18+에서는 별도 설치 없이 사용할 수 있다. 대신 에러 핸들링, 타임아웃, 재시도를 전부 직접 구현해야 한다. 이 글에서 만든 wrapper처럼.

ky는 fetch 위에 얇은 레이어를 올린 라이브러리다. HTTPError를 자동으로 throw하고, 재시도와 타임아웃이 내장되어 있다. 번들 사이즈도 작다. fetch 기반이니까 AbortSignal도 그대로 쓸 수 있다.

선택 기준
번들 사이즈가 중요한 프론트엔드 → `ky`. 서버사이드 데이터 파이프라인에서 세밀한 제어가 필요 → 네이티브 fetch + 커스텀 wrapper. 이미 axios가 깔려 있는 레거시 → 굳이 바꿀 필요 없다.
## 흔한 실수와 트러블슈팅

TypeScript fetch API 에러 핸들링에서 자주 보이는 실수 몇 가지를 정리한다.

response.json()을 두 번 호출하는 실수

Response 객체의 body는 한 번만 읽을 수 있다. ReadableStream이라서 그렇다. .json()을 호출한 뒤 다시 .text()를 호출하면 TypeError: body used already 에러가 난다.

// ❌ 이렇게 하면 두 번째 호출에서 에러
const json = await response.json();
const text = await response.text(); // TypeError: body used already

// ✅ 텍스트로 받아서 직접 파싱
const text = await response.text();
let json: unknown;
try {
  json = JSON.parse(text);
} catch {
  console.error("파싱 실패. 원본:", text.slice(0, 500));
}

HTTP 에러 응답의 body를 로깅하면서 동시에 에러 객체에도 담고 싶으면, .text()로 한 번 읽고 JSON.parse()를 직접 호출하는 방식이 안전하다.

AbortController 재사용 실수

한번 abort()AbortController의 signal은 이미 aborted 상태다. 같은 controller를 다음 요청에 재사용하면 즉시 취소된다.

// ❌ controller를 루프 바깥에서 만들면 재사용 문제
const controller = new AbortController();
for (let i = 0; i < 3; i++) {
  // 첫 번째 요청에서 abort() 후, 두 번째부터 즉시 실패
  await fetch(url, { signal: controller.signal });
}

// ✅ 매 요청마다 새 controller 생성
for (let i = 0; i < 3; i++) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);
  try {
    await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

CORS 에러와 fetch의 동작

CORS 에러가 발생하면 fetch는 TypeError를 던진다. 네트워크 에러와 같은 처리 경로다. 브라우저 콘솔에는 CORS 에러 메시지가 뜨지만, JavaScript 코드에서는 보안상 구체적인 이유를 알 수 없다. error.message"Failed to fetch" 정도로만 나온다.

서버사이드(Node.js)에서는 CORS가 아예 적용되지 않으니 이건 브라우저 환경 한정 이슈다. web.dev의 fetch 에러 핸들링 가이드에서 이 동작을 자세히 설명하고 있다.

앞으로 할 것

개인적으로는, 간단한 API 호출이라도 TypeScript fetch API 에러 핸들링을 Result 패턴으로 감싸는 게 맞다고 생각한다. try/catchunknown 타입 에러를 다루는 것보다 discriminated union으로 에러를 분류하는 편이 코드 안전성 면에서 비교가 안 된다. 타입 시스템이 에러 처리를 강제하니까 런타임 사고가 줄어든다.

이 패턴이 자리 잡히면 다음 단계로 Zod를 사용한 API 응답 런타임 검증을 붙여볼 만하다. as T 타입 단언 대신 Zod 스키마로 응답을 파싱하면 타입 안전성이 컴파일 타임에서 런타임까지 확장된다. 그리고 여러 API를 병렬로 호출하는 데이터 파이프라인에서는 Promise.allSettled()와 이 fetch wrapper를 조합하는 패턴도 고민해볼 만하다. 각 API 호출의 성공/실패를 독립적으로 처리하면서 전체 파이프라인이 하나의 실패로 중단되지 않게 만들 수 있다.

관련 글