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
JavaScript
// 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',
});
}
}