Axios 인터셉터로 간단하게 JWT 토큰 자동으로 담아 보내기

두 번째 라이브러리 게시글입니다! 

 

이 인터셉터도 이전의 모달과 마찬가지로 프로젝트를 진행할 떄마다 코드를 짜는게 너무 귀찮았어요. 

 

거의 모든 프로젝트들이 JWT 토큰을 사용하기 때문에 인터셉터도 항상 같을 수밖에 없었어요. 그래서 이 라이브러리를 만들게 됐답니다.

 

 

깃허브 레포지토리 주소

https://github.com/nyeonseok/nyeonseok-interceptor

 


 

 

먼저 사용할 수 있는 환경에 대해 말해볼게요.

  1. JWT 토큰을 사용하는 환경
  2. access token, refresh token을 모두 사용하는 환경
  3. refresh token은 httpOnly 설정된 환경

이렇게 3 가지를 만족하면 이 라이브러리를 사용할 수 있어요!

 

그럼 어떻게 사용하는지 알아보겠습니다.

 

 

1. 설치


라이브러리를 사용하기 위해 먼저 라이브러리를 설치해야겠죠? 이번 라이브러리도 npm으로 설치해요!

다음 줄을 터미널에 작성합니다.

# 프로젝트에 axios 설치
npm install axios

# nyeonseok-interceptor 라이브러리 설치
npm install nyeonseok-interceptor

 

npm이 이미 설치되어 있다면 두 번째 줄만 작성하면 됩니다.

 

 

 

2. createApiClient import


createApiClient는 제가 만든 인터셉터의 함수 이름이에요. import 해줘야 쓸 수 있으니 인터셉터를 쓰려는 페이지 상단에서 다음 줄을 작성해주세요!

import { createApiClient } from "nyeonseok-interceptor";

 

 

 

3. 기본 설정


다들 인터셉터에 대해 알고 계시겠지만, 라이브러리 사용 이해를 위해 간략히 설명해볼게요.

 

JWT 기반 인증/인가 로직은 다음과 같습니다.

  1. 로그인 성공 시, 서버에서 access token, refresh token을 발급해줌.
  2. 클라이언트는 access token을 보통 localStorage, sessionStorage에 저장함. refresh tokenhttpOnly 설정하여 쿠키로 저장 (XSS 공격 방지)
  3. 이후에 서버와 API 통신할 때마다 header에 access token을 담아서 보냄 => 인증
  4. 서버는 받은 access token을 가지고 해당 API/리소스에 접근할 권한이 있는지 판단 => 인가
  5. 만약 access token이 만료됐다면 access token을 갱신해야 함. 이때 refresh token을 보내서 access token 갱신 요청.
  6. 갱신 성공한다면 다시 access token을 보내서 인증 & 인가.
  7. 갱신 실패한다면 보통 로그아웃 처리.

보통 이런 흐름으로 진행됩니다. 

 

 

이 설명을 한 이유는 프로젝트마다 설정하는 게 다 다르기 때문이에요. 바로 예시 설정을 보여드릴게요.

const api = createApiClient({
  // .env에 정의한 기본 경로 변수명 작성, 하드코딩으로 바로 기본 경로 작성해도 무관
  baseURL: import.meta.env.VITE_BASE_URL,,

  // AccessToken을 가져오는 함수 작성, 여기선 localStorage에 저장하고 가져오는 방식 채택
  getAccessToken: () => localStorage.getItem("accessToken"),

  // 요청 Interceptor로 재발급받은 AccessToken을 저장하는 함수 작성.
  setAccessToken: (token) => localStorage.setItem("accessToken", token),

  // RefreshToken를 통해 access token을 갱신해주는 API 주소 작성
  refreshEndpoint: "/api/auth/refresh",

  // 로그아웃 함수 작성
  onLogout: () => {
    localStorage.removeItem("accessToken");
    window.location.href = "/login";
  },
});

 

우선 설정해야 할 부분은 총 5개에요. 주석에도 적혀 있지만, 조금 더 자세하게 설명해보도록 할게요!

 


 

  • 백엔드 서버 주소를 정의해주면 돼요. 백엔드 서버와 통신하기 때문에 해당 주소를 적어주면 됩니다.
baseURL: import.meta.env.VITE_BASE_URL,,

 

 

  • access token을 가져오는 함수에요. 저는 보통 localStorage에 저장하기 때문에 다음과 같이 작성했어요! 
getAccessToken: () => localStorage.getItem("accessToken"),

 

 

  • refresh token으로 access token을 갱신했을 때 저장하는 함수에요. 
setAccessToken: (token) => localStorage.setItem("accessToken", token),

 

 

  • refresh token으로 access token을 갱신해주는 api 주소에요.
refreshEndpoint: "/api/auth/refresh",

 

 

  • access token 갱신 실패 시 로그아웃 처리 함수에요.
onLogout: () => {
    localStorage.removeItem("accessToken");
    window.location.href = "/login";
  },

 

 

 

다시 말하지만 위 코드는 제가 쓰는 방식으로 작성한 것이기 때문에 얼마든지 자신에게 맞게 작성하시면 돼요.

 

 

4. interceptor 사용


이제 설정할 건 다 했으니 어떻게 사용하는지 알아보겠습니다!

  const fetchUserInfo = async () => {
    try {
      // 통신할 때 앞에 api 붙여주면 됨
      const response = await api.get("api/my-info");
      if (response.status === 200) {
        alert("성공");
      } else {
        const error = await response.data;
        alert(error.message);
      }
    } catch (error: any) {
      const errorMessage =
        error.response?.data?.message ||
        error.message ||
        "알 수 없는 오류 발생";
      alert(errorMessage);
    }
  };

 

사용자 정보를 불러오는 비동기 함수를 작성해봤어요. 받아온 값을 설정하는 부분은 제외하고 전체적인 흐름을 볼 수 있도록 작성했습니다!

 

결론부터 말하면 

const response = await api.get("api/my-info");

 

여기서 api. 를 붙인 게 interceptor을 적용한 거에요.

 

 

그럼 저 코드는 무슨 뜻이냐 하면, 아까 위에서 정의한 baseURL + "api/my-info" get 요청을 보내는 거에요.

 

그리고 그 이후에 로직은 아까 interceptor 인증/인가에서 설명한 것과 같이 흘러갑니다.

 

 

5. 예시 코드


import { createApiClient } from "nyeonseok-interceptor";

export default function Profile() {
  const api = createApiClient({
    baseURL: import.meta.env.VITE_BASE_URL,
    getAccessToken: () => localStorage.getItem("accessToken"),
    setAccessToken: (token) => localStorage.setItem("accessToken", token),
    refreshEndpoint: "/api/auth/refresh",
    onLogout: () => {
      localStorage.removeItem("accessToken");
      window.location.href = "/login";
    },
  });

  const fetchUserInfo = async () => {
    try {
      const response = await api.get("api/my-info");
      if (response.status === 200) {
        alert("성공");
      } else {
        const error = await response.data;
        alert(error.message);
      }
    } catch (error: any) {
      const errorMessage =
        error.response?.data?.message ||
        error.message ||
        "알 수 없는 오류 발생";
      alert(errorMessage);
    }
  };
  return <></>;
}

 

createApiClient에서 설정한대로 interceptor은 동작합니다!

 

 

6. 마무리


이렇게 두 번째 라이브러리 게시글도 끝났습니다.

 

간단하게 사용하길 바라는 마음에서 배포하게 됐는데 간단하다고 느끼실 지 모르겠네요,,

 

다른 라이브러리를 만들고 또 다른 라이브러리 게시글을 작성하도록 하겠습니다!!

 


 

혹시라도 사용하다가 오류가 있거나 개선하면 좋겠는 점이 있다면 말해주세요!

 

감사합니다!