[Next.js] Axios 역할을 대신하는 Custom Fetch 함수를 만들어보자

D uuu·2024년 11월 1일
0

Next.js

목록 보기
5/5
post-thumbnail

왜 Custom Fetch 를 만들어야 할까?

제목을 보고 들어왔다면, 굳이? 라는 의문이 들 수 있습니다. Axios 와 같은 좋은 라이브러리가 있는데 왜 굳이 Fetch를 커스터마이징해야 할까요?

Next.js 의 Fetch API 확장

Next.js는 서버 요청마다 자동으로 캐싱과 재검증을 관리하는 기능을 제공합니다. 이로써 별도의 캐싱 로직을 작성하지 않아도 데이터를 효율적으로 관리할 수 있습니다. 하지만 한가지 아쉬운 점이 있다면 Next.js 의 고유한 캐싱 및 재검증 메커니즘은 Fetch API 에 의존한다는 점입니다. 따라서 Axios 를 사용하려면 Next.js 의 자동 캐시 기능은 포기하거나 직접 캐싱 및 재검증 로직을 구현해야 합니다.

https://nextjs.org/docs/app/api-reference/functions/fetch

선택의 갈림길 ✅

Next.js의 자동 캐싱 기능은 상당히 유용하기 때문에 단순히 Axios를 사용하기 위해 이를 포기하는 것은 비효율적이라는 생각이 듭니다. 그러나 한편으로는 Axios 의 편리함을 경험한 개발자라면, Fetch 의 반복적인 URL 설정, 에러 처리, 공통 헤더 추가와 같은 작업들이 번거롭게 느껴질 수 있습니다. 저 역시 프로젝트에서 fetch 함수를 사용하면서 이러한 반복 작업이 번거롭다고 느껴졌습니다.

그럼 어떻게 해야할까?

공식 문서에서 권장하는 대로 Fetch 를 사용하는 것이 가장 좋은 대안입니다. 하지만 Fetch 를 단순히 사용하는 것만으로는 코드 중복과 관리의 비효율성 문제를 해결하지 못합니다. 결론적으로, Next.js의 장점을 유지하면서 Axios의 생산성을 도입하기 위해 Fetch를 래핑(wrapping)한 Custom Fetch 를 만들기로 결정했습니다.

Custom Fetch에 고려할 점 🧐

Custom Fetch를 설계할 때 구현하고자 했던 주요 기능은 다음과 같습니다.

  1. baseUrl 설정: 각 요청마다 중복적으로 작성할 필요 없도록 기본 URL을 설정합니다.
  2. 공통 headers 설정: 모든 요청에 적용되는 공통 헤더를 설정합니다.
  3. interceptors 설정: 요청과 응답을 가로채는 로직을 통해 토큰 추가, 에러 처리 등의 작업을 처리합니다.
  4. timeout 설정 : 일정 시간이 지나도 응답이 없으면 에러를 던집니다.

Axios 에서 영감을 얻은 Fetch 인스턴스화

Axios 는 axios.create 를 통해 공통 설정을 미리 정의할 수 있습니다. 이를 참고해 Fetch 에도 비슷한 방식으로 인스턴스를 생성할 수 있도록 createFetch 라는 함수를 만들었습니다.

axios.create 방식

const apiClient = axios.create({
  baseURL: 'https://api.example.com', 
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
});

apiClient.interceptors.request.use(
  (config) => {
    // 요청 전 로직 (ex. 인증 토큰 추가)

    return config;
  },
  (error) => {
    // 요청 오류 처리
    return Promise.reject(error);
  }
);

createFetch 방식

const defaultFetch = createFetch({
    baseUrl: 'https://api.example.com',
    headers: {
        'Content-Type': 'application/json',
    },
    interceptors: {
        request: async (args) => {
            // 요청을 가로채서 어떠한 로직을 실행할 수 있다.
  
            return args;
        },
        response: async (response, requestArgs) => {
            // 응답을 가로채서 어떠한 로직을 실행할 수 있다.
 
            return response;
        },
    },
});

createFetch 의 내부 구현

createFetch는 Fetch 요청을 처리하는 커스텀 함수를 생성하여, 요청 및 응답 로직을 중앙에서 관리하고 반복되는 코드 패턴을 줄여줍니다. 이 함수는 baseUrl, defaultHeaders, interceptors를 설정값으로 받아, 이를 기반으로 Fetch 요청을 실행합니다. 생성된 Fetch 함수는 상대 경로와 추가 옵션을 입력받아, 내부 설정값과 결합된 최종 요청을 처리하게 됩니다.

export function createFetch({
    baseUrl = '',
    defaultHeaders = {},
    interceptors = {},
}: FetchOptions) {
    const { request: requestInterceptor, response: responseInterceptor } = interceptors;

    return async (url?: string, options: OptionsType = {}) => {
        const finalUrl = url ? `${baseUrl}${url}` : baseUrl;

        let mergedHeader;

        if (options.headers) {
            mergedHeader = applyDefaultHeaders(defaultHeaders, options.headers);
        }

        const finalOptions: RequestInit = {
            ...options,
            headers: {
                ...defaultHeaders,
                ...mergedHeader,
            },
        };

        let requestArgs: [string, RequestInit] = [finalUrl, finalOptions];

        if (requestInterceptor) {
            requestArgs = await requestInterceptor(requestArgs);
        }

        let response: Response;

        response = await fetch(...requestArgs);

        if (responseInterceptor) {
            response = await responseInterceptor(response, requestArgs);
        }

        return response;
    };
}

프로젝트 내 createFetch 활용 사례

프로젝트에서는 createFetch 를 활용하여 Fetch 요청을 간소화하고 반복 작업을 줄였습니다. 이를 통해 코드의 재사용성을 높이고, 에러 핸들링과 로깅을 일관되게 관리할 수 있었습니다.

1️⃣ 기본 URL 설정
각 요청의 baseUrl을 미리 설정해 두어, API 경로를 개별 요청마다 작성할 필요가 없습니다. 서비스별로 다른 baseUrl을 가진 Fetch 인스턴스를 생성해 사용할 수 있습니다.

2️⃣ 기본 헤더 추가
모든 요청에 공통으로 포함되는 Content-Type: application/json 헤더를 설정했습니다. 추가적으로 특정 요청에서만 필요한 헤더를 제공할 경우, 기본 헤더와 병합되어 최종 요청에 반영됩니다.

3️⃣ 에러 로깅
interceptors 를 이용해 네트워크 에러가 발생할 경우 sendErrorMail 함수를 활용해 에러 위치와 메시지를 메일로 전송하도록 했습니다. 기존에 반복적이고 일관성 없던 에러 핸들링 작업을 인스턴스에 작업함으로써 일관되게 적용됩니다.

createFetch를 활용하여 프로젝트 전반에서 사용할 기본 defaultFetch 함수를 구현했습니다. 이 함수는 일관된 API 호출 방식을 제공하며, 각종 요청과 응답에 대한 공통 처리를 포함합니다.

예를 들어, 사용자 정보를 조회하려면 단순히 다음과 같이 호출할 수 있습니다:

const userData = await defaultFetch('/users');
export const defaultFetch = createFetch({
    baseUrl: `${BASE_URL}/api`,
    defaultHeaders: {
        'Content-Type': 'application/json',
    },
    interceptors: {
        request: async (args: [string, RequestInit]) => {
            const [url, options] = args;

            return args;
        },
        response: async (response: Response, requestArgs: [string, RequestInit]) => {
            if (!response.ok) {
            	const errorMessage = `Fetch request failed with status: ${response.status}`;
                
                const errorOptions = {
                    errorLocation: requestArgs[0],
                    errorMessage: errorMessage,
                };
                sendErrorMail(errorOptions);

                throw new Error(errorMessage);
            }

            return response;
        },
    },
});

기존 코드와의 비교

🚀 기존 Fetch 코드

기존 방식에서는 API 요청을 보낼 때마다 URL, headers 등을 매번 설정해야 했습니다. 또한, fetch는 4xx와 5xx 상태 코드를 자동으로 에러로 처리하지 않기 때문에, 응답이 실패했을 때 data.ok로 상태를 확인하고 따로 에러 처리를 해야 했습니다. 이로 인해 코드가 반복적이고, 에러 핸들링이 일관되지 않아 관리가 어려웠습니다.

export async function updateUser(updateData){
    const data = await fetch(`${API_ENDPOINTS.USERS}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(updateData),
    });

    if (!data.ok) {
        throw new Error('Failed to create Goals');
    }

    const result = await data.json();

    return result;
}

⛳️ Custom Fetch 적용 후

defaultFetch 를 활용하면 baseUrl, 기본 헤더, 에러 처리 로직이 이미 정의되어 있으므로, API 엔드포인트와 옵션만 전달하면 됩니다. interceptors 에서 일관되게 에러 처리가 이루어지므로, 각 요청마다 에러 처리 로직을 중복 작성할 필요가 없어졌습니다. 특히 로직이 변경될 경우, 인스턴스의 로직만 수정하면 이를 사용하는 모든 요청에 자동으로 적용되기 때문에 유지보수 측면에서도 매우 효율적입니다.

export async function updateUser(updateData){
    const data = await defaultFetch(`${API_ENDPOINTS.USERS}`, { method: 'PUT', body: JSON.stringify(updateData) });

    const result = await data.json();

    return result;
}

createFetch 를 사용하면서 느낀 장점은 아래와 같습니다.

1️⃣ 코드 간소화
공통 설정을 중앙에서 관리하여 요청 코드가 간결해지고 가독성이 향상됩니다.

2️⃣ 일관된 에러 핸들링
모든 Fetch 요청에서 동일한 방식으로 에러를 처리하므로 일관성이 생기고 관리가 용이해졌습니다.

3️⃣ 유지보수 효율성
로직 변경 시 Fetch 인스턴스만 수정하면 이를 사용하는 모든 요청에 자동으로 적용되기 때문에 유지보수 측면에서 효율적입니다.

추가 개선할 부분✨

  1. timeout 지원
    custom fetch 를 만들 때의 목표는 타임아웃 기능도 포함되어 있었으나, 아직 구체적인 구현 방법을 결정하지 못했습니다. 향후 개선 사항으로 AbortController 를 활용하여 요청이 일정 시간 내에 응답하지 않으면 자동으로 중단되도록 설정할 계획입니다.

  2. 응답 데이터 파싱
    현재는 데이터를 받아올 때마다 사용자가 직접 JSON으로 변환해야 합니다. 하지만 axios처럼 createFetch 내부에서 자동으로 처리하도록 개선할 예정입니다. 이렇게 하면 반복적인 작업을 줄이고 코드 가독성을 높일 수 있을 것으로 기대됩니다.

  3. 메서드 설정
    현재는 fetch 요청 옵션에 method 를 명시적으로 포함시켜야 합니다. 이 부분을 더 간편하게 사용하기 위해, GET, POST, PUT, DELETE와 같은 HTTP 메서드를 미리 설정된 모듈로 제공할 계획입니다. 이렇게 하면 사용자는 메서드를 직접 지정하지 않아도 되고, 함수 호출만으로 요청을 보낼 수 있어 코드가 더욱 직관적이고 간결해질 것입니다.

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글

관련 채용 정보