axios 모듈화 및 일괄 에러 핸들링 (interceptors 안에서 useNavigate 사용하는 법)

HYERI ·2023년 8월 22일
1

Spport 프로젝트

목록 보기
3/3

axios란?

axios는 JavaScript에서 HTTP 요청을 보내는 데 사용되는 라이브러리이다. 주로 웹 애플리케이션에서 서버와의 통신을 위해 사용되며, 클라이언트 측에서 서버로 데이터를 보내거나 서버로부터 데이터를 받아올 때 유용하게 활용된다.

주요특징

  1. 요청과 응답의 변환: JSON 형식으로 변환해줘야하는 fetch와 달리 axios는 요청과 응답 데이터를 자동으로 JSON 형식으로 변환해준다.
  2. 요청 취소와 타임아웃 설정: 요청을 취소하거나 타임아웃을 설정하여 응답이 오랜 시간동안 오지 않을 경우에도 제어할 수 있다.
  3. HTTP 요청 설정: HTTP 요청의 설정을 자세하게 조정할 수 있다. 헤더, 파라미터, 인증 정보 등을 설정하여 다양한 요청을 보낼 수 있다.
  4. 인터셉터(interceptor): 요청과 응답을 인터셉트(intercept)하여 중간에 로직을 추가하거나 수정할 수 있다. 예를 들어, 요청 전에 토큰을 추가하거나 응답을 가공할 수 있다.
  5. Promise 기반: 비동기 작업을 처리하기 위해 Promise를 사용하므로 코드의 가독성을 높일 수 있다.

axios 도입 이유

처음에 axios와 fetch의 차이점을 잘 알지 못하고 나를 포함한 모든 팀원들에게 익숙한 fetch를 사용하기로 했다. fetch를 사용할때 그래도 코드가 반복되는게 싫어서 CommonAPI.js 파일을 만들어 미리 사용할 api 메서드들을 정의해서 사용했다.

// CommonAPI.js 일부 (fetch 사용)
/* ---------------------------- GENERAL API */
const API_NO_BODY = async (method, token, reqUrl) => {
  const res = await fetch(url + reqUrl, {
    method: method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-type': 'application/json',
    },
  });
  const json = await res.json();
  return json;
};

const API_BODY = async (method, token, reqUrl, bodyData) => {
  const res = await fetch(url + reqUrl, {
    method: method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-type': 'application/json',
    },
    body: JSON.stringify(bodyData),
  });
  const json = await res.json();
  return json;
};

/* ---------------------------- GET API */
const GET_API = async (token, reqUrl) => {
  return await API_NO_BODY('GET', token, reqUrl);
};

/* ---------------------------- POST API */
const POST_API = async (token, reqUrl, bodyData) => {
  return await API_BODY('POST', token, reqUrl, bodyData);
};

✔️ 코드의 반복 최소화: 위의 메서드들을 살펴보면 매번 token을 넣어야 했고, 메서드를 불러오는 파일에서 매번 recoilState를 사용해서 token을 불러와야했다. 무언가 한번에 설정해줄 수 있는게 있으면 api 파일 뿐만 아니라 다른 파일에서도 많은 코드 수를 줄일 수 있을 것 같았다 => axios의 interceptors을 사용하면 token을 받아오거나 없애는 순간 authorization을 설정해 줄 수 있어서 코드가 많이 간략해질 것을 기대했다.
✔️ 일괄 에러 처리: api 관련해서 에러가 났을 때 일일이 처리해주는 것이 번거로워서 잠깐 미루고 있었다.=> axios의 interceptors을 사용하면 조금 더 쉽고 간단하게 에러 핸들링을 할 수 있을 것을 기대했다.


axios 인스턴스 생성과 모듈화

인스턴스 생성과 defaults 설정

import axios from 'axios';

const instance = axios.create({
  baseURL: 'https://api.example.com',
});

instance.defaults.headers.post['Content-Type'] = 'application/json';
instance.defaults.headers.put['Content-Type'] = 'application/json';
  • axios의 create 메서드를 사용하여 인스턴스를 생성한다. baseURL은 모든 요청에 앞으로 추가될 기본 URL을 설정한다. 이렇게 하면 각 요청에서 baseURL을 반복해서 입력하지 않아도 된다.

  • instance.defaults.headers.post['Content-Type'] = 'application/json';: 생성한 axios 인스턴스의 defaults.headers를 사용하여 기본 헤더를 설정한다.

  • 이렇게 생성한 instance를 사용하면 baseURL과 헤더 설정이 기본적으로 포함된 상태로 요청을 보낼 수 있다.

모듈화

이미 대부분의 코드를 위의 CommonAPI를 사용했기에 CommonAPI에 있는 메서드들은 최대한 유지하면서 axios를 사용하기로 했다.

import api from './index';

/* ---------------------------- GET API */
const GET_API = async (reqUrl) => {
  try {
    const response = await api.get(reqUrl);
    return response.data;
  } catch (error) {
    console.error(error);
  }
};

const POST_API = async (reqUrl, bodyData = {}) => {
  try {
    const response = await api.post(reqUrl, bodyData);
    return response.data;
  } catch (error) {
    console.error(error);
  }
};

효과

  • 원래의 CommonAPI 파일과 비교하면 많이 간략해진 것을 확인할 수 있었다.
  • headers의 defaults 세팅값을 미리 정해줘서 많은 중복을 줄일 수 있었다.
  • interceptors를 통해 Authorization도 정해줄 것이기 때문에 token을 파라미터로 넘기지 않아도 되서 recoilState를 다른 파일에서 import하지 않아도 됐다.
  • 그 결과 많지는 않지만 약 100줄의 코드를 줄일 수 있었고, 가독성 또한 좋아졌다.

axios interceptors request

  • instance.interceptors.request.use는 axios 인스턴스에서 요청을 보내기 전에 요청(config) 객체를 가로채어 처리하는 기능을 제공하는 메서드이다. 이를 통해 요청 전에 원하는 작업을 수행하거나 요청 헤더를 수정하는 등의 작업을 할 수 있다.
instance.interceptors.request.use(
  (config) => {
    const token = JSON.parse(localStorage.getItem('recoil-persist')).userToken;
    if (token) {
      config.headers['Authorization'] = 'Bearer ' + token;
    } else {
      delete config.headers['Authorization'];
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  },
);
  • config: 현재 요청의 설정 정보를 포함하고 있는 객체
    • localStorage.getItem('recoil-persist').userToken을 통해 로컬 스토리지에서 유저 토큰을 가져온다.
    • 가져온 토큰이 존재하면, 해당 토큰을 요청의 헤더에 'Authorization' 키로 추가한다. 이를 통해 서버에게 유저가 인증되었음을 알릴 수 있다
    • 가져온 토큰이 존재하지 않으면, 요청 헤더에서 'Authorization' 키를 제거한다.
    • 수정된 config 객체를 반환하여 수정된 설정을 적용한다.
  • error: 현재 요청의 에러 정보를 포함하고 있는 객체
    • 인터셉터 함수 내에서 발생한 에러를 처리하기 위한 부분이다.
    • 이 부분에서는 에러를 거절된 프로미스로 반환하여 에러가 연쇄적으로 전파되도록 한다.

token 설정할 때 interceptors를 사용해야 하는 이유

  • 나는 이 interceptors.request.use를 token이 recoil-persist를 통해 localStorage에 저장됐을때 headers의 Authorization을 설정해주는데 사용했다.
  • 처음에는 위의 defaults 값을 설정해주었을때처럼 instance.defaults.headers['Authorization'] = 'Bearer ' + token; 이런 식으로 설정해주려했지만 그 순간만 설정될 뿐 계속 지속되지 않았다.
  • interceptors을 사용하면 해당 인터셉터 함수가 요청을 보낼 때마다 실행되므로 동적으로 요청을 가로채고 수정하기에 계속 해당 token이 설정이 되어있는 것을 확인할 수 있었다.

axios interceptors response

  • instance.interceptors.response.use는 axios 인스턴스에서 서버 응답이 도착했을 때 처리할 로직을 정의할 수 있다.
  • 나는 이를 통해 error가 났을 경우 모두 '/error' 페이지로 navigate 해줘서 에러 핸들링을 하기로 했다.

기본 코드

instance.interceptors.response.use(
  (response) => response,
  (error) => {
      // error 처리
    return Promise.reject(error);
  },
);
  • 여기서 error 함수 안에 error가 났을 경우 useNavigate를 사용해 에러페이지로 이동시킬려고 하였다. 하지만 여기서 문제가 일어났다.

문제

Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

  • 내가 이 글을 쓰게한 이유이다...
  • React로 작업하면서 거의 매번 보는 에러였다. interceptors 안에서 useNavigate를 사용할 수 없었지만 useNavigate를 사용하지 않고 다른 페이지로 이동할 방법이 생각나지 않았다.
  • 한국 블로그에서도 크게 해결법을 찾지 못했지만 stackoverflow에 다행히도 나같은 문제가 발생한 사람들이 있었다.

interceptors.response.use 안에서 useNavigate 사용법

react-router-dom의 unstable_HistoryRouter 사용

  • react-router-dom에서 제공하는 HistoryRouter이다.
  • history 라이브러리의 createBrowserHistory를 사용해서 history 객체를 정의하고 HistoryRouter로 감싼 후 나중에 history 객체의 특정 메서드 사용해 다른 페이지로 이동할 수 있다.
  • 조금 더 간단하게 구현은 할 수 있으나 추천하지 않는다.
  • unstable이기에 안정성을 보장하지 못하고 애초에 현재 react-router-dom 버전에서 삭제되어서 특정 버전을 사용하지 않는 이상 사용할 수 없다.
  • stackoverflow에서 많은 사람들이 언급하였기에 추가해보았다.

interceptor 컴포넌트 분리 후 useNavigate를 props로 전달

  • 내가 사용한 방법이다!
  • hook을 결국 인스턴스와 설정을 해준 파일 안에서 사용할 수 없으므로 interceptor.response 설정은 따로 컴포넌트를 분리한다.
import { useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import instance from './index';

export default function AxiosNavigation() {
  const navigate = useRef(useNavigate());

  useEffect(() => {
    const intercetpor = instance.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.code === 'ERR_NETWORK') {
          navigate.current('/error', {
            state: {
              error_code: error.code,
            },
          });
        }
        if (error.response?.status < 200 || error.response?.status > 299) {
          navigate.current('/error');
        }
        return Promise.reject(error);
      },
    );

    return () => {
      instance.interceptors.response.eject(intercetpor);
    };
  }, []);
  return <></>;
}
  1. useRef와 useNavigate를 사용하여 네비게이션을 수행할 navigate 객체를 참조로 저장한다.
  2. useEffect를 통해 컴포넌트가 마운트될 때 응답 인터셉터를 설정한다.
  3. 인터셉터는 응답이 성공인 경우에는 그대로 반환하고, 실패인 경우에는 오류 코드나 응답 상태 코드에 따라 다른 동작을 수행한다.
  4. 네트워크 오류인 경우, 오류 코드에 따라 오류 페이지로 이동한다.
  5. 응답 상태 코드가 200에서 299 범위를 벗어난 경우, 일반적인 오류 페이지로 이동한다.
  6. 컴포넌트가 언마운트되는 시점에 응답 인터셉터를 제거한다.
// index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { QueryClient, QueryClientProvider } from 'react-query';
import { HelmetProvider } from 'react-helmet-async';
import AxiosNavigation from './api/AxiosNavigation';

const container = document.getElementById('root');
const queryClient = new QueryClient();

const root = createRoot(container);
root.render(
  <BrowserRouter basename={process.env.PUBLIC_URL}>
    <AxiosNavigation />
    <HelmetProvider>
      <RecoilRoot>
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      </RecoilRoot>
    </HelmetProvider>
  </BrowserRouter>,
);
  • 아무 곳이나 <BrowserRouter> 안에 위치시킨다.

참고 자료

0개의 댓글