에러 처리
ApiCookError와 TransformError를 구분해 처리하는 방법과 각 에러의 실제 발생 조건.
SDK가 던지는 에러는 두 종류입니다. 다만 실패의 상당수는 예외가 아니라 조용한 빈 값으로 나타나므로, 그 구분을 아는 것이 먼저입니다.
두 가지 에러 클래스
import { ApiCook, ApiCookError, TransformError } from '@apicook/sdk';
ApiCookError— 설정 문제, 인증 실패, 원본 API 호출 실패 등 대부분의 경우.TransformError— 가공 규칙을 적용하다 예외가 난 경우.slug필드로 어느 엔드포인트인지 알 수 있습니다.
TransformError는 ApiCookError를 상속하지 않습니다. 둘은 형제 관계라서, 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에는 code도 status도 없습니다. 메시지 문자열이 전부입니다.
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는 그 값을 노출하지 않습니다.
재시도할 가치가 있는 것은 일시적 실패뿐입니다. timeout과 fetch는 다시 시도할 만하지만, 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에 실패가 담깁니다. 성공 응답도 검사해서 로깅해야 조용한 부분 실패를 놓치지 않습니다.
증상별 원인 대조표는 문제 해결에 정리해 두었습니다.