본문 바로가기

JavaScript

[JavaScript] axios 인터셉터로 전역 에러 처리하기

들어가며

API 요청 시 발생하는 에러를 각 컴포넌트에서 개별적으로 처리하다 보면 코드가 중복되고 관리가 어려워집니다. axios 인터셉터를 활용하면 이러한 에러 처리를 전역적으로 깔끔하게 관리할 수 있습니다.

axios 인터셉터 설정하기

1. 기본 인스턴스 생성

// src/api/axios.ts
import axios from 'axios';

export const instance = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
  },
});

2. 요청(Request) 인터셉터

instance.interceptors.request.use(
  (config) => {
    // 요청 보내기 전 수행할 작업
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    // 요청 에러 처리
    return Promise.reject(error);
  }
);

3. 응답(Response) 인터셉터

instance.interceptors.response.use(
  (response) => {
    // 응답 데이터 가공
    return response.data;
  },
  (error) => {
    // 에러 응답 처리
    return handleAxiosError(error);
  }
);

에러 처리 유틸리티 만들기

1. 에러 타입 정의

// src/types/error.ts
export interface ApiError {
  status: number;
  message: string;
  code?: string;
}

export class CustomError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'CustomError';
  }
}

2. 에러 핸들러 구현

// src/utils/errorHandler.ts
import { AxiosError } from 'axios';
import { ApiError } from '../types/error';

export const handleAxiosError = (error: AxiosError) => {
  if (error.response) {
    // 서버가 응답을 반환한 경우
    const status = error.response.status;

    switch (status) {
      case 400:
        handleBadRequest(error.response.data as ApiError);
        break;
      case 401:
        handleUnauthorized();
        break;
      case 403:
        handleForbidden();
        break;
      case 404:
        handleNotFound();
        break;
      case 500:
        handleServerError();
        break;
      default:
        handleUnexpectedError();
    }
  } else if (error.request) {
    // 요청은 보냈지만 응답을 받지 못한 경우
    handleNetworkError();
  } else {
    // 요청 설정 중 에러가 발생한 경우
    handleUnexpectedError();
  }

  return Promise.reject(error);
};

3. 상황별 에러 처리 함수

// src/utils/errorHandler.ts
const handleBadRequest = (error: ApiError) => {
  toast.error(error.message || '잘못된 요청입니다.');
};

const handleUnauthorized = () => {
  toast.error('로그인이 필요합니다.');
  // 로그인 페이지로 리다이렉트
  window.location.href = '/login';
};

const handleForbidden = () => {
  toast.error('접근 권한이 없습니다.');
};

const handleNotFound = () => {
  toast.error('요청한 리소스를 찾을 수 없습니다.');
};

const handleServerError = () => {
  toast.error('서버 에러가 발생했습니다. 잠시 후 다시 시도해주세요.');
};

const handleNetworkError = () => {
  toast.error('네트워크 연결을 확인해주세요.');
};

const handleUnexpectedError = () => {
  toast.error('예기치 못한 에러가 발생했습니다.');
};

실제 사용 예시

1. API 요청 함수 작성

// src/api/user.ts
import { instance } from './axios';

export const userApi = {
  getProfile: async (userId: string) => {
    try {
      const response = await instance.get(`/users/${userId}`);
      return response;
    } catch (error) {
      // 전역 에러 처리기가 동작하므로 여기서는 추가 처리 불필요
      throw error;
    }
  },
};

2. 컴포넌트에서 사용

// src/components/Profile.tsx
import { userApi } from '../api/user';

function Profile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  const fetchProfile = async () => {
    try {
      const data = await userApi.getProfile(userId);
      setUser(data);
    } catch (error) {
      // 전역적으로 처리되므로 특별한 경우가 아니면 추가 처리 불필요
      console.error(error);
    }
  };

  useEffect(() => {
    fetchProfile();
  }, [userId]);

  return (/* JSX */);
}

고급 활용

1. 재시도 로직 추가

const retryConfig = {
  retries: 3,
  retryDelay: (retryCount: number) => retryCount * 1000, // 1s, 2s, 3s
};

instance.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    const originalRequest = error.config;

    if (error.response.status === 500 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        return await instance(originalRequest);
      } catch (retryError) {
        return Promise.reject(retryError);
      }
    }

    return handleAxiosError(error);
  }
);

2. 토큰 리프레시 로직

instance.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    const originalRequest = error.config;

    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const newToken = await refreshToken();
        localStorage.setItem('token', newToken);
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return instance(originalRequest);
      } catch (refreshError) {
        // 리프레시 토큰도 만료된 경우
        handleUnauthorized();
        return Promise.reject(refreshError);
      }
    }

    return handleAxiosError(error);
  }
);

마무리

axios 인터셉터를 활용한 전역 에러 처리는 다음과 같은 이점이 있습니다:

  1. 코드 중복 감소
  2. 일관된 에러 처리
  3. 유지보수 용이성
  4. 사용자 경험 향상

에러 처리는 프로덕션 환경에서 매우 중요한 부분이므로, 체계적인 에러 처리 시스템을 구축하는 것이 좋습니다.