매번 서비스마다 새로운 레포로 init 을 하고 있는데,도전해보고 싶은 과제들이 있었다.
그 중 하나는 fetch 를 커스텀 하는 것이었다. 이전에는 axios 로 interceptor 등의 방법으로 api 를 관리했는데, 삼촌의 SDK 를 접하고(https://nestia.io/docs/) fetch 로 구현해보면서 새로운 서비스에는 fetch 로 안정적으로 구현해보고자 함에 대한 도전의 갈망이 생겼다.
그리하여 효율적인 HTTP 요청을 처리하기 위해, 유연성을 제공하면서도 유지보수성을 생각하여 커스텀 fetch 유틸리티를 구현에 대해서 공유해보고자 한다.
우선, 목표는 다음과 같이 설정했다
단순히 API로부터 데이터를 받아오는 것을 넘어서, 오류 처리와 코드를 유지보수하기 쉽게 만드는 것을 목표로했다.
목표가 아닌 것 : 토큰 만료 관리
-> 사용하는 서비스가 B2B 향이라 토큰을 관리하는 부분은 제외했다.
에러 핸들링, HTTP 요청(GET, POST, PUT) 및 토큰 관리 등 각기 다른 역할을 수행하는 모듈로 구성된 fetcher 유틸리티를 만들었다.
먼저, 모든 요청에서 일관된 방식으로 오류를 처리하기 위해 타입 안전한 에러 핸들러를 정의했다.
ErrorHandler 함수는 오류가 발생할 때 중앙에서 이를 처리하도록 하여, 코드 내에서 반복되는 try-catch 블록을 줄였다.
export type ErrorHandler = <Data>(
error: Error,
ongoingRequest: { url: string; config: RequestInit },
) => Promise<Data>;
export type AdAPIError = {
detail: string;
instance: string;
status: number;
title: string;
type: string;
};
다음으로, 요청을 수행하고 발생한 오류를 처리하는 유틸리티 함수 makeRequest
를 만들었다.
이 함수는 URL을 구성하고, 응답을 처리하며, 오류를 중앙에서 처리하여 일관된 방식으로 오류에 대응할 수 있도록 했다.
export const makeRequest = async <T = Response>(url: string, config: RequestInit): Promise<T> => {
const fullUrl = `${process.env.BASE_API_URL}${url}`;
return fetch(fullUrl, config)
.then(async res => {
if (res.ok) {
const text = await res.text();
return text ? JSON.parse(text) : {};
} else {
const errorDetails = await res.json();
throw { status: res.status, ...errorDetails };
}
})
.catch((error: Error & { apiError?: any }) => {
const errorHandler = getErrorHandler();
return errorHandler<T>(error, { url, config });
});
};
이후, HTTP 메서드(GET
, POST
, PUT
)를 fetcher 객체 내에서 캡슐화하여 코드의 유지보수성과 재사용성을 높였다.
const fetcher = {
get: async <T = Response>(url: string, params?: Record<string, any>, token?: string): Promise<T> => {
const queryParams = params ? new URLSearchParams(params).toString() : "";
const config: RequestInit = {
method: "GET",
headers: new Headers({
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}),
};
return makeRequest<T>(`${url}${queryParams ? `?${queryParams}` : ""}`, config);
},
post: async <T = Response>(url: string, data?: Record<string, any>, token?: string): Promise<T> => {
const config: RequestInit = {
method: "POST",
headers: new Headers({
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}),
body: data ? JSON.stringify(data) : null,
};
return makeRequest<T>(url, config);
},
put: async <T = Response>(url: string, data?: Record<string, any>, token?: string): Promise<T> => {
const config: RequestInit = {
method: "PUT",
headers: new Headers({
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}),
body: data ? JSON.stringify(data) : null,
};
return makeRequest<T>(url, config);
},
};
모듈성은 복잡한 문제를 더 작고 관리하기 쉬운 부분으로 나누는 문제 해결 전략인데 이를 참고했다.
PATCH
나 DELETE
와 같은 새로운 HTTP 메서드를 추가하는 것 용이이러한 아키텍처가 성능과 유지보수성을 향상시키긴 했지만, 몇 가지 우려점도 존재한다.
특징 | fetch | axios |
---|---|---|
기본 타임아웃 지원 | x (AbortController(?) 사용해서 추가구현필요) | 타임아웃 옵션 제공 |
타임아웃 에러 처리 | 수동 (AbortError 처리 필요) | 자동 (타임아웃 에러 발생 및 처리 ) |