Skip to content

API 요청 핸들러 사용하기

Eojin Choi edited this page Jun 8, 2024 · 1 revision

API 요청 핸들러

시즈닝은 axios 모듈을 이용해 백엔드 서버로의 REST API 요청을 처리하고 있으며, 프론트엔드에서 요청과 에러 코드 처리를 돕는 API 요청 핸들러를 분리해 사용하고 있습니다.

// @utils/api/APIService.js
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
  },
  timeout: 15000, // 요청 만료 시간을 15초로 설정합니다.
});

const getAccessToken = () => {
  const accessToken = localStorage.getItem('accessToken');

  if (accessToken) {
    return accessToken;
  } else {
    console.log('* No Access Token... Redirecting to /login');
    throw new Error('No access token');
  }
};

api.interceptors.request.use(
  (config) => {
    try {
      const accessToken = getAccessToken();
      config.headers.Authorization = `Bearer ${accessToken}`;
    } catch (error) {
      console.error('Access token error:', error.message);
      window.location.href = '/login';
      throw new axios.Cancel('Operation canceled');
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      console.log('* Unauthorized... Redirecting to /login');
      window.location.href = '/login';
    } else if (error.response) {
      console.log('* Response Error...');
    }
    return Promise.reject(error);
  }
);

export default api;
  • axios에서 제공하는 요청 인터셉터를 활용해 Access Token의 유효성을 검증합니다.
  • 마찬가지로, 응답 인터셉터를 활용해 응답의 에러 코드에 따른 공통 처리 로직을 수행합니다.
    • 401(: 토큰 만료) - /login 페이지로 이동
    • 기타 에러 코드 - API 요청을 수행한 컨텍스트에서 필요 시 분기 처리

일반 요청

// @utils/loader/HomeLoader.jsx
import api from '@utils/api/APIService';

export async function HomeLoader({ request }) {
  // ...

  const homeResponse = await api.get(
    category === 'year'
      ? `/article/list/year/${year}`
      : `/article/list/term?term=${term}&size=20`
  );
  const termResponse = await api.get(`/solarTerm`);
  const newNotificationResponse = await api.get(`/notification/new`);

  return {
    homeData: homeResponse.data,
    termData: termResponse.data,
    newNotificationData: newNotificationResponse.data,
  };
}
  • React-router에서 제공하는 loader 함수를 활용해 페이지가 렌더링되기 전에 필요한 데이터를 fetch할 수 있습니다.
// @contexts/FeedContext.js
// ...
import api from '@utils/api/APIService';

export const FeedContext = createContext();

export function useFeedContext(loaderData) {
  // ...

  const fetchFeedData = async () => {
    // ...

    try {
      const feedResponse = await api.get(
        `/article/friends?size=${size}&lastId=${lastFeedItemId}`
      );
      if (feedResponse.data.length === 0) {
        setLastFeedItemId(null);
      } else {
        setFeedData((feedData) => [...feedData, ...feedResponse.data]);
        setLastFeedItemId(feedResponse.data.at(-1).article.id);
      }
    } catch (error) {
      console.error(error);
      setLastFeedItemId(null);
    }
  };

  // ...
}
  • APIService의 응답 인터셉터에 의해 Promise가 전달되므로, try-catch 블록 안에 요청을 위치시킬 수 있습니다.

FormData 요청

// @utils/hooks/useArticleForm.jsx
// ...
import api from '@utils/api/APIService';

export default function useArticleForm({ ... }) {
  // ...

  const handleSave = async () => {
    if (!images.length && !contents.some((item) => item.text.trim())) {
      alert('내용을 입력하세요.');
      return;
    }

    try {
      const formData = new FormData();
      // ...

      if (mode === 'write') {
        const writeResponse = await api({
          method: 'POST',
          url: `/article`,
          data: formData,
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        });
        navigate(`/article/${writeResponse.data}`, { replace: true });
      } else if (mode === 'edit') {
        const putResponse = await api({
          method: 'PUT',
          url: `/article/${articleId}`,
          data: formData,
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        });
        navigate(`/article/${articleId}`, { replace: true });
      }
    } catch (error) {
      console.error('Error details:', error);
    }
  };

  return { ... };
}
  • FormData 요청을 보내는 경우, configheaders 객체의 값으로 'Content-Type': 'multipart/form-data'를 전달할 수 있습니다. 이 경우 새롭게 전달된 config가 APIService instance보다 더 높은 우선순위를 가져 설정이 덮어씌워집니다.