API Fetch Retry로직 작성해보기 (with Axios)
JavaScript 비동기 지연처리 방법도 겸사겸사 살펴보기
클라이언트가 Rest API 호출을 할 때 Axios를 사용하는 게 국룰인가봅니다. 저도 사실 프로덕션 환경에서 다른 걸 쓰려고 시도해보지 않았던 것 같아요.
회사에서 맡고 있는 서비스 모니터링을 하면서 이유는 잘 모르겠지만 서버 API 콜을 하고 response를 받지 못하는 경우가 어쩌다 한 번씩 발생하는 것을 발견했습니다. 무시하기엔 신경쓰여서 axios의 interceptor를 이용해서 API 콜을 다시 하는 retry로직을 작성해서 배포한지 이제 일주일 정도 되었는데.. 아직까진 같은 에러가 발생하진 않는 것 같아요(에러 잡은듯..? 🤔)
axios-retry라는 라이브러리를 써도 되는데, github star가 아직 조금 적다는 생각이 들어서 프로덕션에서는 사용하진 않았습니다.
대략적인 흐름과 Axios Interceptors
Interceptors(인터셉터)라는 뜻은 가로채는
라는 뜻입니다. Axios 라이브러리를 이용해서 서버에 Rest API 요청을 보내거나 응답을 받는 과정에서 클라이언트가 처리할 일이 있다면 사용할 수 있습니다. 일종의 미들웨어입니다.
일반적으로 서버와 통신해서 화면을 보여줄 때는 이런 로직을 짜게 될텐데요.
- 서버에 요청을 보낸다
- 요청에 성공하면 response를 가지고 화면을 그려준다.
- 요청에 실패(== HTTP status가 2XX이 아닐 때)했다면, 에러화면을 보여준다.
Axios Interceptors는 요청을 보내기 전, 요청을 보냈지만 HTTP status가 2XX가 아닐 때 에러 처리를 할 때 사용하게 됩니다. Axios Interceptors를 사용했을 때를 다시 표현해보자면..
- 서버에 요청을 보내기 전에
Axios Interceptor에서 어떤 작업을 하고
, 요청을 보낸다. - 요청에 성공하면 response를 가지고 화면을 그려준다.
- 요청에 실패(== HTTP status가 2XX이 아닐 때)했다면,
Axios Interceptor에서 어떤 작업을 하고
, 에러 화면을 보여준다.
Axios interceptors는 보통 아래와 같은 형태로 사용합니다. 예시는 response에 대한 Interceptor를 적용했을 때입니다.
import axios from 'axios';const onFulfilled = (response) => {
// HTTP status가 2XX일 때 처리하고 싶은 로직이 있다면 여기에서 처리함
return response
};const onRejected = (error) => {
// HTTP status가 2XX이 아닐 때 여기를 통과하게 됨
// return은 항상 Promise.reject(error)로 해야함
return Promise.reject(error);
};axios.interceptors.response.use(onFulfilled, onRejected);
비동기 지연 처리
이 글을 읽고 계신 여러분이 더 잘 아시겠지만, JavaScript는 Event loop라는 것을 통해서 Non-blocking하게 비동기 처리를 할 수 있다는 강점이 있는 언어입니다(전 강점이라고 생각해여!). 반대로 이야기하면 기본적으로 Single Thread인 언어이기 때문에 쓰레드를 계속 점유하는 상태(blocking)로 코드를 짜면 비효율적입니다. Thread만 일을 시키지 말고, Event Loop도 일을 시키는 게 효율적이에요.
import timeprint("A") // A라는 글자를 print하고,
time.sleep(5) // 5초 뒤에
print("B") // B라는 글자를 print합니다.
위 예제는 파이썬 코드입니다. 파이썬 모듈 중 time.sleep은 대표적인 blocking 함수에요. blocking을 이야기하고 싶었던 것은 사실 아니고, 지연
이라는 개념을 이야기 하고 싶었어요.
print
는 javscript로 치면 console.log
함수인데, A 로그가 찍히고 5초 뒤에 B 로그가 찍힙니다. time.sleep(5) 처럼 다음 줄의 코드가 실행되는 것을 의도적으로 지연시키는 것이 필요한 순간도 있습니다.
위 그림은 제가 이번에 Retry로직을 짠 것을 간단히 그려본 것입니다. 여기서 나름(?) 핵심은 retry 처리를 할 때 비동기 지연을 시켰다는 것입니다.
retry에서 3초, 6초, 9초 이런 식으로 지연시간을 둔 것은 지금 서버가 어떤 상태인지 알 수 없기 때문입니다. 간격을 두고 요청을 보내서 3번 중 한 번은 꼭 응답을 받고 말겠어! 라는 마음을 담은 것이에요.
3초, 6초, 9초 이렇게 지연을 시킬 때 Thread를 blocking하지 않으면서, 지연 효과를 낼 수 있게 하는 것이 중요하겠죠.
JavaScript에서 비동기 지연처리하는 방법은 간단해요. Promise와 setTimeout을 이용하면 됩니다.
const delay = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(); // 5초 뒤에 Promise를 resolve 시킨다는 뜻
}, 5000);
});
}
코드로 적어주세요.
Axios 팀에서도 Retry를 작성할 때는 interceptor를 이용하라는 답변을 해주고 있습니다.
위에서 파이썬 코드의 time.sleep(5) 느낌을 갖도록 axios에 응용해본다면 아래와 같습니다.
아래 코드형태로 짜면 로그가 찍히는 순서는 아래와 같습니다. Promise를 사용했기 때문에 thread를 blocking하지 않게 지연효과를 누릴 수 있습니다.
- ‘A’ 로그가 찍힘
- 5초 뒤 ‘retry’ 로그가 찍힘
- ‘B’라는 로그가 찍힘
하지만 만약에 retry라는 함수에서 new Promise()라는 형태를 안쓰고 바로 setTimeout을 쓴다면 로그가 찍히는 순서는 아래와 같습니다.
- ‘A’로그가 찍힘
- ‘B’로그가 찍힘
- 5초 뒤 ‘retry’ 로그가 찍힘
import axios from 'axios';const customAxiosInstance = () => {
const axiosInstance = axios.create();
const onFulfilled = (response) => response; const retry = (errorConfig) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('retry');
resolve(axiosInstance.request(errorConfig));
}, 5000);
});
} const onRejected = (error) => {
if (error.config) {
return retry(error.config);
}
return Promise.reject(error);
}; axiosInstance.interceptors.response.use(
onFulfilled,
onRejected,
); return axiosInstance;
};try {
console.log('A');
const apiRequest = customAxiosInstance();
const response = await apiRequest.get(API_URL);
} catch {
console.log('B');
}
몇 초마다 요청하는게 좋은가요?
Exponential backoff 또는 백오프 알고리즘 이런 키워드로 구글에 검색하면 많은 검색결과를 보실 수 있습니다.
개인적인 생각으로는 클라이언트단에서는 setTimeout(()=>{}, 0)
이런 형태(딜레이 타임이 0)인 경우만 피하면 괜찮다고 생각합니다.
규칙적으로 3초마다 retry하는 것도 괜찮은 방법일 수도 있습니다. 하지만 현재 서버가 어떤 상태일지 알 수 없는 상황에서 retry 시도할 때마다 지연시간을 늘리는 형태가 3번 중 한 번이라도 200 OK를 받을 수 있지 않을까..?라는 생각을 해서 3초뒤 retry, 6초 뒤 다시 retry, 9초 뒤 다시 retry 이런 형태로 구현했습니다.
interceptors.response.use는 여러 번 사용할 수도 있나요?
결론부터 말하면 여러 개의 interceptor를 사용해도 됩니다. 이런 형태가 가능하다는 이야기에요. API 콜을 했는데 에러가 발생하면, onRejected1
,onRejected2
, onRejected3
함수가 차례로 실행됩니다.
axios.interceptors.response.use(onFulfilled1, onRejected1);
axios.interceptors.response.use(onFulfilled2, onRejected2);
axios.interceptors.response.use(onFulfilled3, onRejected3);
Axios의 interceptor 구현 코드를 살펴보면, interceptors의 prototype에 use라는 함수를 만들었는데, 이 함수를 이용해서 handler라는 array안에 onFulfilled, onRejected 함수를 모아두고, forEach를 돕니다.(여기서 forEach함수는 axios에서 따로 utils에 만들어둔 forEach함수를 이용하고 있는데, 정확히는 for문으로 array를 탐색합니다.)
(또다른 팁) Axios v0.21.1에서 AxiosError라는 타입가드가 추가됨
TypeScript를 쓰다보면 라이브러리에서 미리 타입을 정의해준 경우가 참 고마울 때가 있는데, 0.21.1 버전을 사용하시면 AxiosError 타입을 사용할 수 있습니다.
import axios, { AxiosError, AxiosResponse } from 'axios';
const onFulfilled = (response: AxiosResponse) => response;const onRejected = (error: AxiosError) => Promise.reject(error);
axios.interceptors.response.use(onFulfilled, onRejected);