[ETC] Axios interceptors

2023. 7. 15. 15:03ETC

Axios 인터셉터는 HTTP 요청이나 응답을 가로채서 그 전후에 어떤 작업을 수행하도록 하는 기능

요청을 보내기 전에 헤더를 수정하거나 응답을 처리하기 전에 데이터를 변환하는 것과 같은 작업을 수행할 수 있다.

나의 경우 JWT 엑세스 토큰을 발급, 헤더에 담아 사용을 하였고 페이지 리프레쉬 과정에서 해당 토큰이 유실되는 이슈로 인해서 아래의 인터셉터를 활용했다.

구현 로직은 아래의 코드를 참조

// 인증 헤더를 설정하는 요청 인터셉터
axios.interceptors.request.use(
	request => {
		const token = store.get('jwtToken') // store는 로컬 스토리지나 쿠키를 관리하는 라이브러리로 가정
		if (token) {
			request.headers['Authorization'] = `Bearer ${token}`
		}
		return request
	},
	error => {
		return Promise.reject(error)
	}
)

// 토큰 만료를 처리하는 응답 인터셉터
axios.interceptors.response.use(
	response => {
		return response
	},
	async error => {
		const originalRequest = error.config
		if (error.response.status === 401 && !originalRequest._retry) {
			// 401 Unauthorized
			originalRequest._retry = true // 토큰 갱신 후 재요청을 피하기 위한 플래그 설정
			const newToken = await refreshJwtToken() // 토큰을 갱신하는 함수로 가정
			store.set('jwtToken', newToken) // 갱신된 토큰을 저장
			axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}` // 기본 헤더를 갱신된 토큰으로 설정
			return axios(originalRequest) // 원래의 요청을 다시 보냄
		}
		return Promise.reject(error)
	}
)

위의 코드에서 요청 인터셉터는 모든 요청에 대해 인증 헤더를 설정하고, 응답 인터셉터는 401 Unauthorized 에러를 감지하여 토큰을 갱신하고 원래의 요청을 다시 보내는 작업을 하고 있고, 재요청을 피하기 위해 플래그 설정을 하였음.

refreshJwtToken 로직의 경우 직접 구현 필요

정리 : 유실 된 엑세스 토큰을 특정 엔드포인트 요청에 사용을 할 경우 해당 요청을 가로채 토큰의 유무를 확인하고, 갱신 이후에 처리하는 과정으로 해당 이슈를 처리했다.

추가 이슈!

추가로 위 기능을 사용하던 도중 초기 브라우져 렌더링 과정에서 여러건의 요청이 있을 경우 여러개의 새 토큰을 발급하게 되어 토큰이 충돌하거나 잘못되어서 무한 루프에 빠지는 이슈가 있었음.

이 문제를 해결하기 위해 리프레쉬 토큰 요청이 진행중인지, 추적하는 상태를 추가했고 이미 진행중이라면 이후 요청에 대해서는 이전 요청의 결과를 기다리도록 처리 했음.

let isRefreshing = false // 현재 토큰을 새로 고침 중인지 나타내는 플래그
let failedQueue: any = [] // 실패한 요청들을 저장할 배열

// 처리 대기중인 요청들을 처리하는 함수
const processQueue = (error: any, token = null) => {
	failedQueue.forEach((prom: any) => {
		if (error) {
			prom.reject(error) // 토큰 새로 고침 요청이 실패하면 모든 요청을 거부
		} else {
			prom.resolve(token) // 토큰 새로 고침 요청이 성공하면 새 토큰과 함께 모든 요청을 해결
		}
	})

	failedQueue = [] // 처리 완료 후 대기열을 초기화
}
// 응답 인터셉터 설정
instance.interceptors.response.use(
	response => {
		return response
	},
	error => {
		const originalRequest = error.config
		if (error.response.status === 401 && !originalRequest._retry) {
			if (isRefreshing) {
				// 이미 토큰 새로 고침 요청이 진행 중인 경우
				return new Promise(function (resolve, reject) {
					failedQueue.push({ resolve, reject }) // 처리 대기중인 요청들을 대기열에 추가
				})
					.then(token => {
						originalRequest.headers['Authorization'] = 'Bearer ' + token
						return axios(originalRequest) // 토큰 새로 고침 요청이 성공하면 원래의 요청을 다시 실행
					})
					.catch(err => {
						return Promise.reject(err)
					})
			}

			originalRequest._retry = true
			isRefreshing = true

			return new Promise(function (resolve, reject) {
				refreshToken()
					.then(token => {
						if (!token) {
							window.location.href = '/' // 만약 access_token이 없거나 리프레쉬 토큰도 만료된 경우
						}
						axios.defaults.headers.common['Authorization'] = 'Bearer ' + token
						originalRequest.headers['Authorization'] = 'Bearer ' + token
						processQueue(null, token) // 새 토큰으로 대기중인 모든 요청 재실행
						resolve(axios(originalRequest))
					})
					.catch(err => {
						processQueue(err, null) // 에러가 발생하면 대기중인 모든 요청을 거부
						reject(err)
					})
					.then(() => {
						isRefreshing = false
					}) // 토큰 새로 고침이 완료되면 플래그를 다시 false로 설정
			})
		}
		return Promise.reject(error)
	}
)

'ETC' 카테고리의 다른 글

[ETC] HMR (Hot Module Replacement)  (0) 2023.08.01
[ETC] 트레일링 슬래시(trailing slash)  (0) 2023.07.27
[ETC] EXPO를 통한 안드로이드 앱 패키징  (0) 2023.03.02
[ETC] Pinata Sub Marined  (0) 2023.02.03
[ETC] 서버리스(Serverless)?  (0) 2022.12.07