Axios Interceptor 란?
HTTP 요청이나 응답을 가로채고 수정할 수 있는 중간 단계를 의미합니다. 이를 통해 전역적으로 요청과 응답을 처리하거나 특정 요청이나 응답에 대해 특별한 로직을 적용할 수 있습니다. Interceptor를 사용하면 다양한 용도로 활용할 수 있으며, 예를 들어 토큰을 자동으로 추가하거나 에러를 처리하는 등의 작업을 수행할 수 있습니다. 이를 통해 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.
Axios 공식 문서도 참고해보자 ↓
https://www.npmjs.com/package/axios/v/1.4.0#interceptors
axios
Promise based HTTP client for the browser and node.js. Latest version: 1.6.8, last published: 7 days ago. Start using axios in your project by running `npm i axios`. There are 121723 other projects in the npm registry using axios.
www.npmjs.com
interceptor를 사용하는 이유는 Axios를 사용하여 API 요청을 보낼 때 인증 및 인증 오류 처리를 간편하게 관리하기 위함이다.
1. API요청 시 사용자 인증을 위한 토큰을 헤더에 담아줄 것이고,
2. 사용자 인증에 실패한다면 재인증을 위한 로직을 실행하도록 하고, (e.g. 토큰 재발행, 재로그인)
3. 토큰 발급이 새로 되었다면 이전 요청을 재시도하거나 / 토큰 발급 실패시 사용자에게 다시 로그인 할 것을 요청할 것이다.
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import useAuthStore from '@/states/useAuthStore'
import { useRouter } from 'next/navigation'
const useAxiosWithAuth = () => {
const accessToken = useAuthStore.getState().accessToken
const router = useRouter()
const axiosInstance = axios.create({
baseURL: "base api url",
})
//무한 요청 방지 flag
let isRefreshing = false
// request interceptor
axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`
}
return config
},
(error) => {
return Promise.reject(error)
},
)
// response interceptor
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
return response
},
async (error) => {
const currentPageUrl = window.location.pathname
if (error.response?.status === 401) {
// unauthorized 오류 받았을 때 = 로그인을 하지 않았거나 토큰 만료인 상태
// 엑세스 토큰이 없거나(로그인x) 갱신을 위한 리프레시 토큰이 없거나 이미 토큰 재발급시도를 했었다면
if (!accessToken || isRefreshing) {
// 로그아웃 후 리디렉션
useAuthStore.getState().logout(isRefreshing)
// 로그인 후 사용자를 원래 페이지로 이동시켜주기 위함
router.push('/login?redirect=' + currentPageUrl)
} else {
// 리프레시 토큰으로 엑세스 토큰 재발급 시도
isRefreshing = true
try {
// accessToken 갱신 요청
const response = await axiosInstance.get('재발급/api주소', {
withCredentials: true,
}) // 쿠키를 같이 전달할 수 있는 옵션
const newAccessToken = response.data.accessToken
// 새 엑세스 토큰을 받았다면 토큰을 새로 저장한다
useAuthStore.getState().login(newAccessToken)
// 헤더에 새 토큰을 넣어
error.config.headers['Authorization'] = `Bearer ${newAccessToken}`
// 이전 요청을 재시도
return axios.request(error.config)
} catch (refreshError) {
// 리프레시토큰도 만료되었다는 응답을 받았다면
// 로그아웃 후 로그인 페이지로
isRefreshing = true
useAuthStore.getState().logout(isRefreshing)
router.push('/login?redirect=' + currentPageUrl)
}
}
}
return Promise.reject(error)
},
)
return axiosInstance
}
export default useAxiosWithAuth
그리고 전역상태 관리를 위한 useAuthStore.tsx
Zustand 라이브러리를 사용했다.
import { create } from 'zustand'
import LocalStorage from './localStorage'
import axios from 'axios'
interface IAuthStore {
// 로그인 상태
isLogin: boolean
accessToken: string | null
// 엑세스 토큰 저장 함수
login: (accessToken: string) => void
// 로그아웃 함수
logout: (isRefreshing?: boolean) => void
}
const useAuthStore = create<IAuthStore>((set) => {
// 로컬에 저장된 토큰 가져오기
const authDataJSON = LocalStorage.getItem('authData')
const authData = authDataJSON
? JSON.parse(authDataJSON)
: { accessToken: null }
const API_URL = process.env.NEXT_PUBLIC_CSR_API
return {
isLogin: !!authData.accessToken,
accessToken: authData.accessToken,
// 로그인 함수
login: (accessToken) => {
const authDataToSave = { accessToken }
// 로컬 스토리지에 authData: { accessToken: `토큰`} 형태로 저장
LocalStorage.setItem('authData', JSON.stringify(authDataToSave))
// 전역 상태 업데이트
set(() => ({
isLogin: true,
accessToken,
}))
},
// 로그아웃 함수
logout: (isRefreshing) => {
// (사용자가 의도한 로그아웃일 때 로그아웃 api 호출)
if (authData.accessToken && isRefreshing === undefined) {
axios
.get(`로그아웃/api/`, {
headers: {
Authorization: `Bearer ${authData.accessToken}`,
},
})
.catch(() => {
// console.log('만료된 토큰') -- do nothing
})
}
// 로컬 스토리지에서 authData 삭제
LocalStorage.removeItem('authData')
// 전역 상태 업데이트
set(() => ({
isLogin: false,
accessToken: null,
}))
},
}
})
export default useAuthStore
여기서 LocalStorage를 보면 localStorage와 철자가 다른 것을 볼 수 있는데, 사실 이 클래스도 다시 만들었다.
Next.js환경에서 ssr(Server-Side-Rendering)되는 페이지의 localStorage를 감지할 수 없기 때문...
LocalStorage.ts
class LocalStorage {
constructor() {}
static setItem(key: string, value: string) {
if (typeof window !== 'undefined') {
localStorage.setItem(key, value)
}
}
static getItem(key: string) {
if (typeof window !== 'undefined') {
return localStorage.getItem(key)
}
return null
}
static removeItem(key: string) {
if (typeof window !== 'undefined') {
localStorage.removeItem(key)
}
}
}
export default LocalStorage
이렇게 해주면 window객체 존재여부를 확인함으로써 서버 사이드인지 확인하고 오류를 방지할 수 있다.
'Front-End' 카테고리의 다른 글
[React / Next.js + Zustand] StompJS로 채팅 구현하기 (2) - 실시간 채팅 방 상태 관리하기 (2) | 2024.04.05 |
---|---|
[React / Next.js] StompJS로 채팅 구현하기 (1) - 웹 소켓 연결 (0) | 2024.03.29 |
[Next.js + Vanilla Extract css + Storybook] 공용 컴포넌트 만들기 (feat. 절대경로) (0) | 2024.03.15 |
[Peer 스터디] 웹 접근성에 대해 알아보자... (0) | 2024.03.15 |
컴포넌트 랜덤 분포 애니메이션 구현하기 (0) | 2024.03.08 |