UNPKG

api-wizard

Version:

A powerful TypeScript wrapper for native Fetch API with token management, interceptors, and type-safe HTTP requests

219 lines (218 loc) 7.73 kB
// Fetch 에러 클래스 (axios 호환) export class FetchError extends Error { constructor(status, statusText, originalResponse, url, data, requestConfig) { super(`HTTP Error ${status}: ${statusText}`); this.name = 'FetchError'; this.status = status; // axios 호환 response 구조 this.response = { data, status, statusText, headers: originalResponse.headers }; // request 정보 (axios 호환) this.request = { url, method: requestConfig?.method || 'GET' }; this.config = requestConfig; } } // Fetch 클라이언트 구현 export class FetchClientImpl { constructor(config) { this.baseURL = config?.baseURL || ''; // config.headers에 Content-Type이 없으면 기본값으로 추가하지 않음 this.defaultHeaders = config?.headers || {}; this.defaultConfig = { credentials: 'include', ...config?.requestConfig }; // 기본값: 500번대만 에러로 처리 (현재 동작 유지) this.defaultValidateStatus = config?.validateStatus || ((status) => status < 500); } // URL 생성 헬퍼 buildURL(url, params) { const fullURL = this.baseURL + url; if (!params || Object.keys(params).length === 0) { return fullURL; } // undefined, null, 빈 문자열 값들을 필터링하고 문자열로 변환 const filteredParams = Object.entries(params).reduce((acc, [key, value]) => { if (value !== undefined && value !== null && value !== '') { acc[key] = String(value); } return acc; }, {}); if (Object.keys(filteredParams).length === 0) { return fullURL; } const searchParams = new URLSearchParams(filteredParams); return `${fullURL}?${searchParams.toString()}`; } // 헤더 병합 헬퍼 mergeHeaders(...headers) { const result = new Headers(); headers.forEach(headerInit => { if (!headerInit) return; if (headerInit instanceof Headers) { headerInit.forEach((value, key) => result.set(key, value)); } else if (Array.isArray(headerInit)) { headerInit.forEach(([key, value]) => result.set(key, value)); } else { Object.entries(headerInit).forEach(([key, value]) => { if (value !== undefined) result.set(key, value); }); } }); return result; } // Response를 FetchResponse로 변환 (axios 호환 에러 처리) async transformResponse(response, requestConfig) { let data; const contentType = response.headers.get('content-type'); try { if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } } catch (parseError) { data = null; } // validateStatus를 사용하여 에러 처리 // 요청별 validateStatus가 있으면 우선 사용, 없으면 기본값 사용 const validateStatus = requestConfig.validateStatus || this.defaultValidateStatus; // validateStatus가 false를 반환하면 에러로 throw if (!validateStatus(response.status)) { throw new FetchError(response.status, response.statusText, response, response.url, data, requestConfig); } return { data, status: response.status, statusText: response.statusText, headers: response.headers, url: response.url, ok: response.ok }; } // 공통 request 메서드 async request(config) { const { url, params, timeout, baseURL, ...fetchConfig } = config; const finalURL = this.buildURL(url, params); const headers = this.mergeHeaders(this.defaultHeaders, fetchConfig.headers); const requestConfig = { ...this.defaultConfig, ...fetchConfig, headers }; // 타임아웃 처리 const controller = new AbortController(); if (timeout) { setTimeout(() => controller.abort(), timeout); } requestConfig.signal = controller.signal; try { const response = await fetch(finalURL, requestConfig); return await this.transformResponse(response, { ...config, url: finalURL }); } catch (error) { if (error instanceof FetchError) { throw error; } // 네트워크 에러 (axios 호환) const networkError = new Error(`Network Error: ${error instanceof Error ? error.message : 'Unknown error'}`); networkError.request = { url: finalURL, method: requestConfig.method || 'GET' }; networkError.config = { ...config, url: finalURL }; throw networkError; } } // GET 메서드 async get(url, config) { return this.request({ ...config, url, method: 'GET', }); } // POST 메서드 async post(url, data, config) { const headers = this.mergeHeaders(this.defaultHeaders, config?.headers); const contentType = headers.get('content-type') || ''; let body; if (data) { if (contentType.includes('application/x-www-form-urlencoded')) { // form-urlencoded인 경우 그대로 전달 (이미 URLSearchParams.toString()으로 처리됨) body = typeof data === 'string' ? data : new URLSearchParams(data).toString(); } else { // JSON인 경우 JSON.stringify 적용 body = JSON.stringify(data); } } return this.request({ ...config, url, method: 'POST', body, }); } // PUT 메서드 async put(url, data, config) { const headers = this.mergeHeaders(this.defaultHeaders, config?.headers); const contentType = headers.get('content-type') || ''; let body; if (data) { if (contentType.includes('application/x-www-form-urlencoded')) { body = typeof data === 'string' ? data : new URLSearchParams(data).toString(); } else { body = JSON.stringify(data); } } return this.request({ ...config, url, method: 'PUT', body, }); } // PATCH 메서드 async patch(url, data, config) { const headers = this.mergeHeaders(this.defaultHeaders, config?.headers); const contentType = headers.get('content-type') || ''; let body; if (data) { if (contentType.includes('application/x-www-form-urlencoded')) { body = typeof data === 'string' ? data : new URLSearchParams(data).toString(); } else { body = JSON.stringify(data); } } return this.request({ ...config, url, method: 'PATCH', body, }); } // DELETE 메서드 async delete(url, config) { return this.request({ ...config, url, method: 'DELETE', }); } }