Axios Interceptor 기능을 Fetch API로 구현하여 JWT 토큰 처리하기

ClydeHan·2024년 12월 13일
11

JavaScript Fetch API Logo Image

본문을 이해하기 위한 간단한 사전 지식

1. Fetch API란 무엇인가?

Fetch API는 브라우저에 내장된 네트워크 요청을 위한 JavaScript 인터페이스이다. Promise 기반으로 설계되어 HTTP 요청을 쉽게 처리할 수 있다.

Promise: 비동기 작업의 상태를 나타내는 객체로, 작업의 성공(resolve)과 실패(reject)를 처리할 수 있다. 일반적으로 HTTP 요청의 성공과 실패를 처리하는 데 사용된다.


Fetch API의 주요 특징

  1. Promise 기반: 네트워크 요청의 성공과 실패를 .then.catch로 처리한다.
  2. 호환성: 최신 Node.js(버전 18 이상)와 브라우저에서 설치없이 기본적으로 동작한다.
  3. 유연한 요청 옵션: GET, POST 등의 HTTP 메서드와 헤더, 본문 등을 자유롭게 설정할 수 있다.
  4. 기본 제공 기능만 지원: Axios와 달리 Interceptor 등의 고급 기능은 제공하지 않는다.

Fetch API 예제

// Fetch API를 사용하여 특정 ID(1)의 게시물을 가져오기
fetch("https://jsonplaceholder.typicode.com/posts?id=1", {
  method: "GET", // HTTP 메서드 설정 (기본값이 GET)
  headers: {
    "Authorization": "Bearer token", // 인증 토큰 추가
  },
})
  .then((response) => {
    // HTTP 응답 상태 확인
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    // JSON 형식으로 응답 파싱
    return response.json();
  })
  .then((data) => {
    // 성공적으로 가져온 데이터 출력
    console.log("Fetched Data:", data);
  })
  .catch((error) => {
    // 네트워크 오류 또는 HTTP 오류 처리
    console.error("Fetch error:", error);
  });

2. Axios란 무엇인가?

Axios는 Promise 기반 HTTP 클라이언트 라이브러리로, 브라우저와 Node.js 환경 모두에서 동작한다. REST API와의 통신을 단순화하기 위해 설계되었으며, 간결한 문법과 강력한 기능을 제공한다.


Axios의 주요 특징

  1. Promise 기반 비동기 처리: .then.catch를 사용하여 HTTP 요청의 성공과 실패를 처리한다.
  2. 자동 JSON 변환: 요청과 응답 데이터를 JSON 형식으로 자동 변환한다.
  3. 요청 취소 및 시간 초과 설정: 요청을 취소하거나 시간 초과를 설정할 수 있다.
  4. Interceptor 지원: 요청과 응답의 흐름에 개입하여 공통 작업을 처리할 수 있다.
  5. 브라우저와 Node.js 환경에서 모두 사용 가능: 서버와 클라이언트 모두에서 사용할 수 있다.

Axios 예제

// Axios를 사용하여 특정 ID(1)의 게시물을 가져오기
axios
  .get("https://jsonplaceholder.typicode.com/posts", {
    params: {
      id: 1, // 쿼리 파라미터로 ID 전달
    },
    headers: {
      "Authorization": "Bearer token", // 인증 토큰 추가
    },
  })
  .then((response) => {
    // 성공적으로 가져온 데이터 출력
    console.log("Axios Data:", response.data);
  })
  .catch((error) => {
    // 네트워크 오류 또는 HTTP 오류 처리
    if (error.response) {
      console.error("Response Error:", error.response.status, error.response.data);
    } else if (error.request) {
      console.error("No Response:", error.request);
    } else {
      console.error("Error:", error.message);
    }
  });

3. Axios와 Fetch API 비교

Axios와 Fetch API는 모두 HTTP 요청을 처리하기 위한 도구이지만, 기능과 사용 방식에서 차이가 있다. 아래 표는 두 도구의 주요 차이를 정리한 것이다.

항목AxiosFetch API
설치 여부외부 라이브러리 설치 필요 (npm install axios)브라우저 내장, 추가 설치 불필요
Promise 기반지원지원
자동 JSON 처리요청 및 응답 데이터를 자동으로 JSON 변환응답 데이터를 수동으로 .json() 처리 필요
요청 취소AbortController를 사용해 요청 취소 가능 (이전 방식인 CancelToken은 더 이상 권장되지 않음)AbortController를 사용해 요청 취소 가능.
Interceptor 지원요청과 응답의 흐름을 제어할 수 있는 Interceptor 기능 제공.Interceptor는 기본적으로 지원하지 않으나, 커스텀 함수나 유틸리티로 비슷한 패턴을 구현 가능.
호환성브라우저와 Node.js에서 모두 사용 가능최신 Node.js(버전 18 이상)와 브라우저에서 기본적으로 동작.
에러 처리상태 코드가 200-299 범위를 벗어나면 자동으로 에러 처리.상태 코드에 따른 에러 처리를 개발자가 수동으로 구현해야 함 (response.ok 검사 필요).
기능 확장성Interceptor, 요청 취소, 디폴트 설정 등 강력한 기능 제공.단순한 HTTP 요청 처리에 적합하지만, 추가 기능은 커스텀 구현 필요.

4. JWT Token이란 무엇인가?

  • JWT (JSON Web Token)은 클라이언트와 서버 간 인증권한 부여를 위한 JSON 기반 토큰으로, 서버가 상태를 저장하지 않고도 클라이언트를 식별할 수 있게 한다.

Axios Interceptor의 기능

1. Axios Interceptor란?

Axios Interceptor는 HTTP 요청(request)과 응답(response)의 흐름에 개입하여 공통 작업을 수행할 수 있는 Axios의 강력한 기능이다. Interceptor를 통해 요청 전 토큰 추가, 응답 후 에러 처리 등의 작업을 자동화할 수 있다. 즉, 여러 가지 작업을 자동화하고, 코드의 중복을 줄이며, 유지보수성을 높이는 데 사용된다.


Interceptor의 종류

  1. 요청(Request) Interceptor
    • HTTP 요청이 서버로 전송되기 전에 실행된다.
    • 주로 인증 헤더 추가, 로딩 상태 표시 등에 사용된다.
  2. 응답(Response) Interceptor
    • 서버로부터 응답을 받은 후 실행된다.
    • 응답 데이터 변환, 에러 상태 코드 처리 등에 사용된다.

요청(Request) Interceptor 사용 예시

  • 공통 헤더 추가
    • API 요청마다 반복적으로 추가해야 하는 헤더를 Interceptor를 통해 자동으로 설정한다.

      axios.interceptors.request.use((config) => {
        // 요청 헤더에 Content-Type과 Accept-Language를 추가함
        config.headers['Content-Type'] = 'application/json';
        config.headers['Accept-Language'] = 'en-US';
        return config; // 수정된 요청 설정을 반환함
      });
  • 로딩 상태 표시
    • 요청이 시작되면 로딩 애니메이션을 표시하고, 요청이 끝나면 숨기는 로직을 추가할 수 있다.

      axios.interceptors.request.use((config) => {
        // 로딩 애니메이션을 표시함
        showLoadingSpinner();
        return config; // 요청을 그대로 진행함
      }, (error) => {
        // 요청 중 에러가 발생하면 로딩 애니메이션을 숨김
        hideLoadingSpinner();
        return Promise.reject(error); // 에러를 호출한 곳에서 처리하도록 반환함
      });

응답(Response) Interceptor 사용 예시

  • 에러 상태 코드 처리
    • 서버에서 반환된 에러 상태 코드(예: 401, 500)에 따라 클라이언트 로직을 처리한다.

      axios.interceptors.response.use(
        (response) => response, // 성공적인 응답은 그대로 반환함
        (error) => {
          if (error.response && error.response.status === 401) {
            // 401 상태 코드가 발생하면 콘솔에 메시지를 출력하고 로그인 페이지로 리다이렉션함
            console.error("Unauthorized access! Redirecting to login.");
            redirectToLogin();
          }
          return Promise.reject(error); // 에러를 호출한 곳에서 처리하도록 반환함
        }
      );
  • 응답 데이터 전처리
    • 모든 API 응답 데이터를 공통 포맷으로 변환하거나 불필요한 데이터를 제거할 수 있다.

      axios.interceptors.response.use((response) => {
        // 응답 데이터에서 필요한 데이터만 추출하여 반환함
        return response.data;
      });

2. Axios Interceptor로 JWT Token 처리하기

JWT 토큰을 사용한 인증 시스템에서 Interceptor를 활용하면 요청에 자동으로 토큰을 추가하거나, 토큰이 만료되었을 때 재발급을 처리할 수 있다. 다음은 Axios Interceptor를 활용한 JWT 처리 구현이다.


로그인 시 토큰 저장

  • 로그인 성공 시 서버에서 반환된 JWT 토큰을 localStorage에 저장한다.
const login = async (username, password) => {
  // 서버에 로그인 요청을 보내고 사용자 정보를 전달
  const response = await axios.post('/login', { username, password });

  // 응답 데이터에서 JWT 토큰 추출
  const { token } = response.data;

  // JWT 토큰을 localStorage에 저장
  localStorage.setItem('jwt', token);
};

요청에 토큰 자동 추가

  • 모든 API 요청의 Authorization 헤더에 JWT 토큰을 자동으로 추가한다.
  • 이를 통해 모든 요청에서 인증을 간단하게 처리할 수 있다.
axios.interceptors.request.use((config) => {
  // localStorage에서 JWT 토큰을 가져옴
  const token = localStorage.getItem('jwt');

  if (token) {
    // 토큰이 존재하면 Authorization 헤더에 추가
    config.headers.Authorization = `Bearer ${token}`;
  }

  // 수정된 요청 설정 반환
  return config;
});

// 사용 예시
// 사용자 정보 가져오기
const getUserProfile = async () => {
  try {
    // Axios를 통해 GET 요청을 보냄 (Interceptor가 자동으로 Authorization 헤더 추가)
    const response = await axios.get('/api/user/profile');

    // 서버로부터 받은 사용자 프로필 데이터를 출력
    console.log('User Profile:', response.data);
  } catch (error) {
    // 요청 실패 시 에러 메시지를 출력
    console.error('Failed to fetch user profile:', error.message);
  }
};

// 사용 예시
// 새 게시글 작성
const createPost = async (title, content) => {
  try {
    // Axios를 통해 POST 요청을 보냄 (Interceptor가 자동으로 Authorization 헤더 추가)
    const response = await axios.post('/api/posts', { title, content });

    // 서버로부터 받은 새 게시글 데이터를 출력
    console.log('Post Created:', response.data);
  } catch (error) {
    // 요청 실패 시 에러 메시지를 출력
    console.error('Failed to create post:', error.message);
  }
};

토큰 만료 시 갱신

  • 응답에서 401 상태 코드를 감지하여, Access Token이 만료되었을 경우, Refresh Token을 사용해 새로운 Access Token을 발급받고, 이를 이용해 실패한 요청을 재시도한다.
axios.interceptors.response.use(
  (response) => {
    // 성공적인 응답은 그대로 반환
    return response;
  },
  async (error) => {
    if (error.response && error.response.status === 401) {
      // 401(Unauthorized) 상태 코드가 발생하면 Access Token이 만료된 것으로 간주

      // 새 Access Token 발급 요청 (Refresh Token 사용)
      const refreshedToken = await refreshToken(); // refreshToken 함수는 새 Access Token을 서버에서 받아오는 함수

      // 새로 발급받은 JWT 토큰을 localStorage에 저장
      localStorage.setItem('jwt', refreshedToken);

      // 원래 요청을 복제하여 Authorization 헤더를 새 토큰으로 갱신
      const originalRequest = error.config;
      originalRequest.headers.Authorization = `Bearer ${refreshedToken}`;

      // 복제된 요청을 새 Access Token으로 재시도
        return axios(originalRequest);
      } catch (refreshError) {
        // Refresh Token이 만료되었거나, 발급 실패 시 추가 처리
        console.error("Token refresh failed. Redirecting to login.");
        redirectToLogin(); // 로그인 페이지로 리다이렉션
        return Promise.reject(refreshError); // 새로운 에러 반환
      }
    }
    return Promise.reject(error); // 기타 에러는 그대로 반환
  }
);

Fetch API로 JWT Token 처리 구현하기

Fetch APIAxios Interceptor와 같은 내장 기능을 제공하지 않으므로, 수동으로 동작을 구현하는 함수를 작성하여 토큰을 처리한다. Axios Interceptor가 아닌 Fetch API를 사용하여 JWT 토큰 기반 인증을 처리하는 방법은 아래와 같다.


로그인 시 토큰 저장

  • 사용자가 로그인하면 서버는 JWT 토큰을 생성하여 반환하고, 반환된 토큰을 클라이언트 측에 저장하여 이후 인증 요청에서 사용할 수 있도록 한다.
const login = async (username, password) => {
  // 서버에 로그인 요청을 보내고 사용자 정보를 전달
  const response = await fetch('/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ username, password }),
  });

  // 요청이 성공하지 않을 경우 에러를 던짐
  if (!response.ok) {
    throw new Error('Login failed');
  }

  // 응답 데이터에서 JWT 토큰 추출
  const { token } = await response.json();

  // JWT 토큰을 localStorage에 저장
  localStorage.setItem('jwt', token);
};

요청에 토큰 자동 추가

  • 모든 API 요청에 Authorization 헤더에 JWT 토큰을 추가하여 사용자 인증을 처리한다. 이를 통해 개발자는 각 요청마다 토큰을 수동으로 추가할 필요가 없다.
const fetchWithToken = async (url, options = {}) => {
  // localStorage에서 JWT 토큰 가져오기
  const token = localStorage.getItem('jwt');

  // 요청 헤더 설정
  const headers = token
    ? { ...options.headers, Authorization: `Bearer ${token}` } // 토큰이 있으면 Authorization 헤더에 추가
    : { ...options.headers };

  // Fetch API 호출
  const response = await fetch(url, { ...options, headers });

  // 응답 상태 확인: 응답이 실패했을 경우 에러를 던짐
  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }

  // JSON 형식으로 응답 데이터를 반환
  return response.json();
};

// 사용 예시
// 사용자 정보 가져오기
const getUserProfile = async () => {
  try {
    // fetchWithToken 함수를 통해 사용자 정보 GET 요청을 보냄
    const profile = await fetchWithToken('/api/user/profile');

    // 서버로부터 받은 사용자 프로필 데이터를 출력
    console.log('User Profile:', profile);
  } catch (error) {
    // 요청 실패 시 에러 메시지를 출력
    console.error('Failed to fetch user profile:', error.message);
  }
};

// 사용 예시
// 새 게시글 작성
const createPost = async (title, content) => {
  try {
    // fetchWithToken 함수를 통해 새 게시글 작성 요청을 보냄
    const newPost = await fetchWithToken('/api/posts', {
      method: 'POST', // POST 요청 지정
      headers: {
        'Content-Type': 'application/json', // 요청 데이터 형식을 JSON으로 설정
      },
      body: JSON.stringify({ title, content }), // 요청 본문에 게시글 데이터 추가
    });

    // 서버로부터 받은 새 게시글 데이터를 출력
    console.log('New Post Created:', newPost);
  } catch (error) {
    // 요청 실패 시 에러 메시지를 출력
    console.error('Failed to create post:', error.message);
  }
};

토큰 만료 시 갱신

  • 401 Unauthorized 상태 코드를 감지하여 Access Token이 만료된 경우 Refresh Token으로 새 Access Token을 발급받는다. 새 토큰을 발급받은 후, 실패했던 요청을 갱신된 토큰으로 다시 실행합니다.
const refreshToken = async () => {
  // Refresh Token으로 새 Access Token 요청
  const response = await fetch('/refresh-token', {
    method: 'POST',
    credentials: 'include', // HttpOnly 쿠키 사용 시 필요
  });

  // 요청이 실패하면 에러를 던짐
  if (!response.ok) {
    throw new Error('Failed to refresh token');
  }

  // 응답에서 새 Access Token 추출
  const data = await response.json();
  return data.accessToken; // 새 Access Token 반환
};

const fetchWithTokenRetry = async (url, options = {}) => {
  try {
    // 기본 Fetch 요청
    return await fetchWithToken(url, options);
  } catch (error) {
    if (error.message.includes('401')) {
      // 401 상태 코드 처리: 새 Access Token 발급
      const refreshedToken = await refreshToken();
      localStorage.setItem('jwt', refreshedToken);

      // Authorization 헤더 갱신
      options.headers = {
        ...options.headers,
        Authorization: `Bearer ${refreshedToken}`,
      };

      // 갱신된 토큰으로 원래 요청 재시도
      return fetchWithToken(url, options);
    }

    // 기타 에러는 그대로 던짐
    throw error;
  }
};

Axios Interceptor와 Fetch API 비교

기능Axios InterceptorFetch API
토큰 자동 추가Interceptor를 통해 요청에 자동으로 토큰 추가개발자가 직접 작성한 함수로 토큰 추가 로직 구현 필요
401 상태 처리Interceptor로 응답에서 401 상태를 자동 처리수동으로 401 상태 처리 함수를 구현하고 호출해야 함
코드 재사용성Interceptor와 설정을 통해 요청 및 응답의 공통 로직을 간결하게 관리 가능.공통 로직을 관리하려면 커스텀 함수 또는 유틸리티를 작성해야 함.
구현 난이도내장 기능으로 간단히 구현 가능추가적인 로직 작성 및 유지 관리 필요

Fetch API가 적합한 경우

  1. 프로젝트가 가볍고 간단하며, HTTP 요청 라이브러리를 추가로 설치하고 싶지 않을 때.
  2. 브라우저 표준만으로 충분히 동작할 때 (예: Interceptor와 같은 추가 기능이 필요 없거나, 직접 구현 가능할 때).
  3. 라이브러리 의존성을 줄이거나, 번들 크기를 줄이는 것이 중요한 프로젝트에서.

참고문헌

0개의 댓글