custom Fetch 유틸리티: 효율적 요청, 중앙 에러 처리

Soly; 독특하게·2024년 10월 10일
0

Next.js

목록 보기
7/7
post-thumbnail

매번 서비스마다 새로운 레포로 init 을 하고 있는데,도전해보고 싶은 과제들이 있었다.
그 중 하나는 fetch 를 커스텀 하는 것이었다. 이전에는 axios 로 interceptor 등의 방법으로 api 를 관리했는데, 삼촌의 SDK 를 접하고(https://nestia.io/docs/) fetch 로 구현해보면서 새로운 서비스에는 fetch 로 안정적으로 구현해보고자 함에 대한 도전의 갈망이 생겼다.

그리하여 효율적인 HTTP 요청을 처리하기 위해, 유연성을 제공하면서도 유지보수성을 생각하여 커스텀 fetch 유틸리티를 구현에 대해서 공유해보고자 한다.

목표

우선, 목표는 다음과 같이 설정했다
단순히 API로부터 데이터를 받아오는 것을 넘어서, 오류 처리와 코드를 유지보수하기 쉽게 만드는 것을 목표로했다.

  • 타입 안정성: TypeScript를 사용하여 엄격한 타입을 보장
  • 재사용 가능한 오류 처리: 유지보수를 위해 중앙에서 오류 처리
  • 모듈성: 요청 로직을 작은 단위로 분리하여 관리

목표가 아닌 것 : 토큰 만료 관리
-> 사용하는 서비스가 B2B 향이라 토큰을 관리하는 부분은 제외했다.

커스텀 Fetch 유틸리티

에러 핸들링, 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 });
    });
};

Fetcher 함수들

이후, 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);
  },
};

모듈식 아키텍처

모듈성은 복잡한 문제를 더 작고 관리하기 쉬운 부분으로 나누는 문제 해결 전략인데 이를 참고했다.

  • 중앙화된 오류 처리: 공통 오류를 한 곳에서 관리함으로써, 코드의 재사용성을 높이고 유지보수를 쉽게 했다

유지보수성

  • 재사용 가능한 유틸리티: 오류 처리와 요청 생성을 유틸리티로 분리
  • 관심사 분리: 데이터 페칭 로직을 UI와 분리함으로써, API 구조 변경이 UI 코드에 미치는 영향을 최소화
  • 확장 가능성: 프로젝트가 확장될 때, PATCHDELETE와 같은 새로운 HTTP 메서드를 추가하는 것 용이

커스텀 Fetch에 대한 우려점

이러한 아키텍처가 성능과 유지보수성을 향상시키긴 했지만, 몇 가지 우려점도 존재한다.

  • 에러 처리 복잡성: 중앙에서 오류를 처리하는 방식은 장점이 많지만, 각기 다른 API 에러에 맞는 고유한 처리가 필요한 경우 복잡해질 수 있을 듯 하다.
  • Fetch 로직의 과도한 추상화: 요청 로직이 너무 추상화되면, 실제 fetch 요청과 멀어져서 디버깅이 어려울 수 있을 듯 하다.
  • api call timeout : axios 와 다르게 time out에 대한 부분을 커스텀해줘야 한다
특징fetchaxios
기본 타임아웃 지원x (AbortController(?) 사용해서 추가구현필요)타임아웃 옵션 제공
타임아웃 에러 처리수동 (AbortError 처리 필요)자동 (타임아웃 에러 발생 및 처리 )
profile
협업을 즐겨하는 목표지향적인, Front-End 개발자입니다.

0개의 댓글