티스토리 뷰

자바스크립트

Fetch로 추상화한 유틸 함수를 Axios 패키지로 마이그레이션 해보기

YG - 96년생 , 강아지 있음, 개발자 희망 2023. 11. 6. 17:28

fetch를 추상화한 기존의 코드

import { ACCESS_TOKEN_KEY } from '@constants/localStorage';

import { getLocalStorage } from './localStorage';
import { silentLogin } from './login/silentLogin';

const headers = {
  'Content-Type': 'application/json',
};

export const makeFetchHeaders = () => {
  const accessToken = getLocalStorage(ACCESS_TOKEN_KEY);

  if (!accessToken) {
    return headers;
  }

  return {
    ...headers,
    Authorization: `Bearer ${accessToken}`,
  };
};

const makeFetchMultiHeaders = () => {
  const accessToken = getLocalStorage(ACCESS_TOKEN_KEY);

  return {
    Authorization: `Bearer ${accessToken}`,
  };
};

export const getFetch = async <T>(url: string): Promise<T> => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'GET',
    headers: makeFetchHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const originError: Error = JSON.parse(errorText);
    const error = { status: response.status, message: originError.message };

    throw new Error(JSON.stringify(error));
  }

  const data = await response.json();

  return data;
};

export const postFetch = async <T>(url: string, body: T) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(body),
    headers: makeFetchHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

export const putFetch = async <T>(url: string, body: T) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'PUT',
    body: JSON.stringify(body),
    headers: makeFetchHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

export const patchFetch = async <T>(url: string, body?: T) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'PATCH',
    headers: makeFetchHeaders(),
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

export const deleteFetch = async (url: string) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'DELETE',
    headers: makeFetchHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

export const multiPostFetch = async (url: string, body: FormData) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'POST',
    body,
    headers: makeFetchMultiHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

export const multiPutFetch = async (url: string, body: FormData) => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'PUT',
    body,
    headers: makeFetchMultiHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const error: Error = JSON.parse(errorText);

    throw new Error(error.message);
  }
};

 

Axios로 마이그레이션 해보기

 

1. axios 설치

 

 

 

시작하기 | Axios Docs

시작하기 브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리 Axios란? Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트 입니다. 그것은 동형 입니다(동일한 코

axios-http.com

npm i axios -D

 

 

2. getFetch 함수를 axios로 변경해 보기

 

fetch에서는 reponse.ok로 성공, 실패 여부를 확인하여 실패한 요청이더라도 백엔드에서 보낸 실패한 상태값과 이유를 적어서 에러를 관리했다면, axios에서는 error을 catch 하여 에러핸들링하는 방식과 기존의 방식과 비슷한 validateStatus 옵션으로 상태값이 500 이하라면 에러가 나지 않도록 설정하여서 에러 핸들링하는 방식이 있었습니다.

 

저는 코드 가독성이 가장 좋다고 생각하는 validateStatus 옵션으로 마이그레이션 하기로 했습니다.

 

 

 

에러 핸들링 | Axios Docs

에러 핸들링 axios.get('/user/12345') .catch(function (error) { if (error.response) { console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); } else if (error.request) { console.log(error.request); } e

axios-http.com

 

Fetch

export const getFetch = async <T>(url: string): Promise<T> => {
  await silentLogin();
  const response = await fetch(url, {
    method: 'GET',
    headers: makeFetchHeaders(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    const originError: Error = JSON.parse(errorText);
    const error = { status: response.status, message: originError.message };

    throw new Error(JSON.stringify(error));
  }

  const data = await response.json();

  return data;
};

 

Axios (try catch 이용)

export const getFetch = async <T>(url: string): Promise<T> => {
  // await silentLogin();
  try {
    const response = await axios(url, {
      method: 'GET',
      headers: makeFetchHeaders(),
    });

    return response.data;
  } catch (error) {
    const { response } = error as AxiosError;

    if (response) {
      const errorData = response.data as { code: number; message: string };
      const errorObj = { status: response.status, message: errorData.message };

      throw new Error(JSON.stringify(errorObj));
    }

    throw new Error('데이터를 불러오지 못했습니다.');
  }
};

 

Axios (.catch 이용)

export const getFetch = async <T>(url: string): Promise<T> => {
  // await silentLogin();
  const response = await axios<T>(url, {
    method: 'GET',
    headers: makeFetchHeaders(),
  }).catch(error => {
    const { response } = error as AxiosError;

    if (response) {
      const errorData = response.data as { code: number; message: string };
      const errorObj = { status: response.status, message: errorData.message };

      throw new Error(JSON.stringify(errorObj));
    }

    throw new Error('데이터를 불러오지 못했습니다.');
  });

  return response.data;
};

 

Axios (validateStatus 옵션 사용) 

export const getFetch = async <T>(url: string): Promise<T> => {
  // await silentLogin();
  const response = await axios<T>(url, {
    method: 'GET',
    headers: makeFetchHeaders(),
    validateStatus: status => status < 500,
  });

  if (response.status !== 200) {
    const errorData = response.data as { code: number; message: string };
    const error = { status: response.status, message: errorData.message };

    throw new Error(JSON.stringify(error));
  }

  return response.data;
};

 

 

3. 리프레시 토큰 관련된 코드를 axios로 변경해 보기

 

토큰을 가져오는 api 변경

fetch에서 body와 credentials 옵션을 이용하던 것을 axios에서는 data와 withCredentials 옵션으로 설정했습니다. 그리고 토큰에서 발생하는 에러의 경우 axios.interceptor에서 처리되도록 분리하였습니다.

 

Fetch

export const postTokens = async (accessToken: string): Promise<SilentLoginToken> => {
  const response = await fetch(`${BASE_URL}/auth/silent-login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ accessToken }),
    credentials: 'include',
  });

  if (!response.ok) {
    throw new Error('error');
  }

  return await response.json();
};

 

 

Axios

export const postTokens = async (accessToken: string): Promise<SilentLoginToken> => {
  const response = await axios(`${BASE_URL}/auth/silent-login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: { accessToken },
    withCredentials: true,
  });

  return response.data;
};

 

 

interceptor 이용하기

get, post, put, delete, patch 등 다양한 요청을 보낼 때 매번 silentLoigin을 붙이지 않고 한 번에 처리할 수 있었습니다.

 

 

 

인터셉터 | Axios Docs

인터셉터 then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다. axios.interceptors.request.use(function (config) { return config; }, function (error) { return Promise.reject(error); }); axios.interceptors.response.use(f

axios-http.com

 

// 요청을 보내기 전 액세스 토큰이 유효한 지 확인한다.
axios.interceptors.request.use(async config => {
  await silentLogin();

  return config;
});

// 요청을 받기 전 액세스 토큰이 유효한 지 확인한다.
axios.interceptors.response.use(
  async response => {
    await silentLogin();

    return response;
  },
  error => {
    const { response } = error;
    const errorData = response.data;

    throw new Error(errorData.message);
  }
);

 

 

4. 모든 fetch 함수를 axios로 변경해 보기

 

interceptor에서 중복된 에러 핸들링 코드를 적용하니 코드의 양이 많이 줄었고, 가독성이 올라갔습니다.

 

import axios from 'axios';

import { ACCESS_TOKEN_KEY } from '@constants/localStorage';

import { getLocalStorage } from './localStorage';
import { silentLogin } from './login/silentLogin';

const headers = {
  'Content-Type': 'application/json',
};

export const makeFetchHeaders = () => {
  const accessToken = getLocalStorage(ACCESS_TOKEN_KEY);

  if (!accessToken) {
    return headers;
  }

  return {
    ...headers,
    Authorization: `Bearer ${accessToken}`,
  };
};

const makeFetchMultiHeaders = () => {
  const accessToken = getLocalStorage(ACCESS_TOKEN_KEY);

  return {
    Authorization: `Bearer ${accessToken}`,
  };
};

axios.interceptors.request.use(async config => {
  await silentLogin();

  return config;
});

axios.interceptors.response.use(
  async response => {
    await silentLogin();

    return response;
  },
  error => {
    const { response } = error;
    const errorData = response.data;

    throw new Error(errorData.message);
  }
);

export const getFetch = async <T>(url: string): Promise<T> => {
  const response = await axios<T>(url, {
    method: 'GET',
    headers: makeFetchHeaders(),
    validateStatus: status => status < 500,
  });

  if (response.status !== 200) {
    const errorData = response.data as { code: number; message: string };
    const error = { status: response.status, message: errorData.message };

    throw new Error(JSON.stringify(error));
  }

  return response.data;
};

export const postFetch = async <T>(url: string, body: T) => {
  await axios(url, {
    method: 'POST',
    data: body,
    headers: makeFetchHeaders(),
  });
};

export const putFetch = async <T>(url: string, body: T) => {
  await axios(url, {
    method: 'PUT',
    data: body,
    headers: makeFetchHeaders(),
  });
};

export const patchFetch = async <T>(url: string, body?: T) => {
  await axios(url, {
    method: 'PATCH',
    headers: makeFetchHeaders(),
    data: body,
  });
};

export const deleteFetch = async (url: string) => {
  await axios(url, {
    method: 'DELETE',
    headers: makeFetchHeaders(),
  });
};

export const multiPostFetch = async (url: string, body: FormData) => {
  await axios(url, {
    method: 'POST',
    data: body,
    headers: makeFetchMultiHeaders(),
  });
};

export const multiPutFetch = async (url: string, body: FormData) => {
  await axios(url, {
    method: 'PUT',
    data: body,
    headers: makeFetchMultiHeaders(),
  });
};

 

 

 

 

5.  테스트 코드 fetch가 아닌 axios에서도 가능하도록 변경하기

axios의 경우 node 환경도 지원을 하기 때문에 jest에서 잘 작동하는 것을 확인할 수 있었습니다. 그리고 fetch를 node 환경에서 사용할 수 있도록 설치했었던 'whatwg-fetch'를 import 하지 않아도 된다는 점을 알 수 있었습니다.

 

 

jest.setup.js

// import 'whatwg-fetch'; << 삭제해도 무방

import dotenv from 'dotenv';
import { setupServer } from 'msw/node';

import { handlers } from './src/mocks/handlers';

dotenv.config({ path: './.env.test' });

/**
 * 이 코드가 없다면 jest에서 upload-images-converter 패키지에 관한 에러가 발생합니다.
 * 
 *    SyntaxError: Unexpected token 'export'

    > 1 | import { imageConverter } from 'upload-images-converter';
    
    https://github.com/nrwl/nx/issues/7844#issuecomment-1220559108
 */
jest.mock('upload-images-converter', () => ({
  __esModule: true,
}));

export const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});

afterEach(() => {
  server.resetHandlers();
});

afterAll(() => {
  server.close();
});

 

 

fetch를 axios로 마이그레이션 해본 후기

 

1. fetch를 추상화해놓지 않았다면 수십 개의 api 함수를 고쳤을 것을 생각하니 아찔했다. 추상화를 해놔서 axios로 마이그레이션 하기 편했다.

 

2. axios랑 fetch랑 크게 다른 건 없고, axios가 개발자 편의성이 더 좋다는 것을 느꼈다. stringify와 parse의 과정을 생략해 주거나, 반복되는 에러 핸들링 코드를 줄일 수 있고, 그 외에도 반복되는 코드를 줄일 수 있어서 좋았다.

 

3. axios가 번들 사이즈가 없다는 것을 알게 되었다. devDependencies에 설치해서 그런가 해서 다시 dependencies에 설치해 보았는데도 똑같았다. 하지만 번들 사이즈 크기를 확인하는 사이트에서는 용량이 있는 것으로 나와있는데 왜 다른지는 모르겠다. 만약 번들 사이즈가 0kb라면 사용 안 할 이유를 지금은 못 찾을 정도로 사용성이 만족스러웠다. 속도가 조금 느리다고 하는데 체감될만한 성능은 아닌 것으로 알고 있다.

 

 

 

axios ❘ Bundlephobia

Find the size of javascript package axios. Bundlephobia helps you find the performance impact of npm packages.

bundlephobia.com

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함