개인정보처리방침이 2026년 9월 20일부터 변경됩니다. 온라인 맞춤형 광고 도입에 따라 행태정보 수집·이용 및 국외 이전 관련 조항이 추가됩니다.
개정 내용 보기

에러 처리

ApiCookError와 TransformError를 구분해 처리하는 방법과 각 에러의 실제 발생 조건.

SDK가 던지는 에러는 두 종류입니다. 다만 실패의 상당수는 예외가 아니라 조용한 빈 값으로 나타나므로, 그 구분을 아는 것이 먼저입니다.

두 가지 에러 클래스

import { ApiCook, ApiCookError, TransformError } from '@apicook/sdk';
  • ApiCookError — 설정 문제, 인증 실패, 원본 API 호출 실패 등 대부분의 경우.
  • TransformError — 가공 규칙을 적용하다 예외가 난 경우. slug 필드로 어느 엔드포인트인지 알 수 있습니다.

TransformErrorApiCookError를 상속하지 않습니다. 둘은 형제 관계라서, instanceof ApiCookError만 확인하면 TransformError를 놓칩니다.

try {
    const data = await cook.fetch('air-quality', { sidoName: '서울' });
} catch (err) {
    if (err instanceof TransformError) {
        console.error(`가공 실패 (${err.slug})`, err.message);
    } else if (err instanceof ApiCookError) {
        console.error('SDK 오류', err.message);
    } else {
        throw err;  // 그 밖의 예외는 삼키지 않습니다
    }
}

마지막 else가 필요합니다. 잘못된 baseUrl로 인한 TypeError처럼 두 클래스 어디에도 속하지 않는 예외가 나올 수 있습니다.

에러 객체에 없는 것

ApiCookError에는 codestatus도 없습니다. 메시지 문자열이 전부입니다.

catch (err) {
    if (err instanceof ApiCookError) {
        err.status;  // undefined — 존재하지 않습니다
    }
}

원본 API가 429를 줬는지 500을 줬는지 상태 코드로 분기하고 싶다면, 멀티소스 엔드포인트의 _meta.errors[].status가 유일한 통로입니다. 단일 소스에서는 메시지 문자열을 파싱하는 수밖에 없습니다.

원본 API 호출 실패 ('air-quality'): 429 Too Many Requests

프록시 모드도 마찬가지입니다. 서버는 상태 코드와 오류 종류를 응답에 담아 보내지만 SDK는 메시지만 꺼내 씁니다.

가장 흔한 실패는 예외가 아닙니다

가공 엔진은 방어적으로 동작합니다. 경로를 못 찾으면 예외를 던지는 대신 조용히 빈 값을 돌려줍니다.

  • sourceRoot 경로가 없으면 → 결과 전체가 null
  • 매핑한 source 필드가 없으면 → 그 키의 값이 비고, JSON으로 나갈 때 키 자체가 사라짐

그래서 원본 API의 응답 구조가 바뀌어도 TransformError가 아니라 null이 돌아옵니다. try/catch만 걸어 두면 아무 일도 없었던 것처럼 통과합니다.

const data = await cook.fetch('air-quality', { sidoName: '서울' });

if (data === null) {
    // 원본 응답 구조가 바뀌었거나 sourceRoot가 잘못됐습니다
    console.warn('가공 결과가 비어 있습니다');
}

빈 결과를 다루는 코드를 항상 두세요. 이것이 실무에서 가장 자주 만나는 실패 모드입니다.

멀티소스: 부분 실패는 정상 응답입니다

소스가 여러 개인 엔드포인트는 일부가 실패해도 예외가 나지 않습니다. 실패한 소스만 null이 되고 나머지는 그대로 돌아옵니다.

const data = await cook.fetch('dashboard', { weather: {...}, air: {...} });

if (data._meta?.partial) {
    for (const e of data._meta.errors) {
        console.warn(`${e.source} 실패 (${e.kind})`, e.message, e.status);
    }
}

예외가 나는 경우는 두 가지뿐입니다.

ApiCookError: 'dashboard' 필수 소스 호출 실패 — air: 503 Service Unavailable
ApiCookError: 'dashboard' 모든 소스 호출 실패 — weather: 500 ...; air: 503 ...

자세한 동작은 부분 실패 처리에 있습니다.

재시도는 직접 구현해야 합니다

SDK에는 재시도 로직이 전혀 없습니다. 실패하면 즉시 예외가 납니다. 프록시가 429에 재시도 간격을 담아 보내더라도 SDK는 그 값을 노출하지 않습니다.

재시도할 가치가 있는 것은 일시적 실패뿐입니다. timeoutfetch는 다시 시도할 만하지만, 4xx는 같은 요청을 반복해도 결과가 같습니다.

async function fetchWithRetry(slug: string, params: Record<string, string>, tries = 3) {
    for (let i = 0; i < tries; i++) {
        try {
            return await cook.fetch(slug, params);
        } catch (err) {
            const retryable = err instanceof ApiCookError
                && /timeout|fetch failed|50\d/.test(err.message);
            if (!retryable || i === tries - 1) throw err;
            await new Promise((r) => setTimeout(r, 2 ** i * 500));
        }
    }
}

서버에서는 타임아웃을 직접 걸어야 합니다

direct 모드의 단일 소스 호출에는 제한 시간이 없습니다. 원본 API가 응답하지 않으면 그대로 매달립니다. 멀티소스는 소스별로 기본 5초가 걸리고, 프록시 모드는 15초입니다.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
    // 호출부에서 직접 제한합니다
    const data = await Promise.race([
        cook.fetch('air-quality', { sidoName: '서울' }),
        new Promise((_, reject) => {
            controller.signal.addEventListener('abort', () => reject(new Error('timeout')));
        }),
    ]);
} finally {
    clearTimeout(timer);
}

로깅할 때

에러를 그대로 기록하면 원인 파악에 필요한 것이 대부분 남습니다. 다만 두 가지를 기억하세요.

  • TransformError의 메시지에는 이미 [slug]가 붙어 있습니다. err.slug를 따로 찍으면 중복됩니다.
  • 멀티소스에서는 예외가 아니라 _meta.errors에 실패가 담깁니다. 성공 응답도 검사해서 로깅해야 조용한 부분 실패를 놓치지 않습니다.

증상별 원인 대조표는 문제 해결에 정리해 두었습니다.