haileyjpark

[JavaScript] 서버 간 axios 통신과 interceptor로 실패 요청 retry 하기 본문

JavaScript

[JavaScript] 서버 간 axios 통신과 interceptor로 실패 요청 retry 하기

개발하는 헤일리 2023. 3. 12. 23:29
반응형

axios를 사용해 서버 간 통신을 하는 모듈에서 socket hang up 에러가 발생하는 경우가 종종 있었습니다. 기존의 동일 프로덕트 내 다른 서비스에서 동료 개발자 분이 개발해두신 재시도 로직과 여러 블로그들을 참고하여 socket hang up 에러로 인해 요청이 실패했을 때 재시도하는 방법을 알아보았습니다.

 

axios란?

axios는 JavaScript로 작성된 HTTP 클라이언트 라이브러리입니다.

브라우저와 Node.js 환경에서 모두 사용할 수 있으며, Promise를 사용하여 비동기적으로 HTTP 요청을 처리합니다.

 

 

axios의 특징

  • Promise 기반: 비동기적으로 데이터를 요청하고 응답을 받아 처리할 수 있습니다.
  • 요청과 응답의 중간에 interceptor를 사용하여 요청/응답의 전처리와 후처리가 가능합니다.
  • axios는 자동으로 JSON 데이터를 파싱하고, form data와 같은 다른 데이터 형식도 변환합니다.
  • 오류 처리가 편리합니다.
  • CSRF 보호 기능을 내장하고 있습니다.
  • 간단하고 직관적인 API: axios의 API는 매우 직관적이며, RESTful API의 요청과 응답을 쉽게 다룰 수 있습니다.

 

 

axios 인스턴스 만들기

import Axios, { AxiosRequestConfig } from 'axios'

const YOUR_URL = 'https://api.example.com';
const RETRY = { retryCount: 0, retryDelay: 2000 };

// axios 인스턴스 생성
// 모든 요청에 적용되는 설정의 default 값을 전역으로 명시할 수 있다.
const createAxiosInstance = () => {
  const newAxiosInstance = Axios.create({
    baseURL: YOUR_URL,
    timeout: 2500,
    headers: {
      'content-type': 'your content-type',
    },
    retry: RETRY.retryCount,
    retryDelay: RETRY.retryDelay,
    httpAgent: new http.Agent({ keepAlive: true }),
    httpsAgent: new https.Agent({ keepAlive: true }),
  });

  return newAxiosInstance;
};

const axiosInstance = createAxiosInstance();

// Instance를 만든 후  defalut 값을 수정할 수 있다. 
axiosInstance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axiosInstance.defaults.timeout = 5000;

axios 인스턴스는 위와 같이 생성할 수 있습니다. 

모든 요청에 적용되는 설정의 default 값을 전역으로 명시할 수 있고, 주로 서버에서 서버로 axios를 사용할 때 요청 헤더를 명시하는 데 쓰입니다. 인스턴스를 만든 후에 default 값을 수정할 수도 있습니다.

 

저는 아래 옵션들을 사용하여 axios 인스턴스를 생성했습니다. 

  • url : 서버 주소
  • baseURL : url을 상대 경로로 쓸 때 url 맨 앞에 붙는 주소
  • timeout : 요청 timeout이 발동 되기 전 milliseconds의 시간을 요청. timeout 보다 요청이 길어진다면, 요청은 취소된다.
  • headers : 요청 헤더
  • httpAgent /  httpsAgent : node.js에서 http나 https를 요청을 할때 사용자 정의 agent를 정의하는 옵션

 

 

axios 요청 메서드

생성한 axios 인스턴스는 다른 서버에 요청을 보낼 수 있는 메서드를 갖추고 있는데, 크게 두 가지로 나눠볼 수 있습니다.

저는 2번의 방법을 사용했습니다.

 

1. request() 메서드를 사용하는 방법. AxiosRequestConfig에 필요한 설정을 추가하여 요청할 수 있습니다.

2. get(), post() 등 HTTP Method 명과 동일한 이름으로 된 메서드를 사용하는 방법

// 1. request() 사용
const requestConfig: AxiosRequestConfig = {
    url: 'https://api.example.com/api',
    method: 'GET'
}
axiosInstance.request(requestConfig)
    .then((response) => {
        console.log(response.data)
    })

// 2. `METHOD`로 된 메서드 사용 (ex. `get()`, `post()` 등)
axiosInstance.get('https://api.example.com/api')
    .then((response) => {
        console.log(response.data)
    })

 

 

axios Interceptor 

axios Interceptor 적용 프로세스

axios는 HTTP 요청과 응답을 가로채 각 요청과 응답 전에 특정 작업을 수행하여 일종의 미들웨어 역할을 하는 Interceptor를 지원합니다.

axios 인스턴스는 요청과 응답에 대해 각각 Interceptor 관리자를 가지고 있고, 이 관리자들은 요청이나 응답을 가로챌 때 실행할 핸들러를 관리합니다.

우리가 실행하고자 하는 핸들러를 Interceptor 관리자에 등록해두면, axios가 요청을 보내기 직전과 응답을 받은 직후에 미리 등록한 핸들러들을 차례로 실행합니다. 

// request 인터셉터
axiosInstance.interceptor.request.use(onFulfilled, onRejected)

// response 인터셉터
axiosInstance.interceptor.response.use(onFulfilled, onRejected)

 

onFulfilled 핸들러에서 에러가 발생하면, onReject 핸들러가 실행됩니다. 

  • 요청 전 인터셉터에서 onReject 핸들러가 실행되면 요청은 이루어지지 않고 즉시 종료됩니다.
  • 다른 서버로 보낸 요청의 응답이 성공이 아니라면 onRejected 핸들러가 동작하게 됩니다.

 

axios Interceptor 사용

요청을 보내는 서버

// Request에 인터셉터 추가
axiosInstance.interceptors.request.use(
    (config) => {
        console.log('Request: 성공하면 실행되지롱')
        return config
    }, 
    (error) => {
        console.log('Request: 실패하면 실행되지롱')
        return Promise.reject(error)
    }
)

// Response에 인터셉터 추가
axiosInstance.interceptors.response.use(
    (response) => {
        console.log('Response: 성공하면 실행되지롱')
        return response
    }, 
    (error) => {
        console.log('Response: 실패하면 실행되지롱')
        return Promise.reject(error)
    }
)

// 요청을 보냄
axiosInstance.post('https://api.example.com/api/socketHangUp')
    .then((response) => {
        console.log('GET: Data')
    })

응답하는 서버

// 응답하는 서버에서 연결을 끊도록 함
const createSocketHangUp = (ctx) => {
  console.log('ctx.request.body:', ctx.request.body);
  ctx.socket.destroy();
};
router.post('api/socketHangUp', createSocketHangUp);

위와 같이 요청과 응답에 각각 Interceptor를 추가할 수 있습니다. 

저는 socket hang up 에러를 처리하기 위해 정확한 경로로 요청을 보내되, 요청을 받는 서버에서 socket hang up 오류를 발생시켰습니다.

요청에 실패하게 되면 터미널에서 아래와 같은 출력값을 볼 수 있습니다.

Request: 성공하면 실행되지롱
Response: 실패하면 실행되지롱
GET: Data

 

onRejected 핸들러는 반환값으로 Promise 객체인 Promise.reject(error)를 반환합니다.

axios는 요청을 실행하고 나서 응답에 대한 Interceptor 핸들러를 하나씩 실행하는데, 이 핸들러가 반환하는 Promise 객체를 가지고 다음 작업을 실행합니다. 따라서 에러 핸들러가 Promise.resolve()를 반환한다면 실패하지 않은 모습을 보여줄 수도 있습니다.

 

axios Interceptor로 실패 요청 retry 하기

1.  Retry 실행을 위한 함수

const doRetry = async (error, _axiosInstance) => {
  const { config } = error;

  // config가 없거나 config에 retryCount가 없으면 에러 처리
  if (!config || !config.retryCount) {
    return Promise.reject(error);
  }
  // config에 __retryCount(실제 재시도 실행한 횟수)가 없으면 0으로 설정
  config.__retryCount = config.__retryCount || 0;

  // config의 retryCount(재시도 해야할 횟수)보다 __retryCount(실제 재시도 실행한 횟수)가 더 크면 에러 처리
  if (config.__retryCount >= config.retryCount) {
    return Promise.reject(error);
  }
  
  // config에 __retryCount(실제 재시도 실행한 횟수)를 업데이트
  config.__retryCount += 1;

  // delay
  await new Promise((resolve) => {
    setTimeout(() => resolve(), RETRY.retryDelay);
  });
  
  // 재시도
  return _axiosInstance(config);
};

2. Retry를 위한 Axios Response Interceptor

axiosInstance.interceptors.response.use((response) => {
  return response;
}, (error) => {
  // Socket hang up 에러 메시지가 발생되면 1회 retry
  if (error.code === 'ECONNRESET' && error.message === 'socket hang up' && error.config.retryCount === 0) {
    _.set(error.config, 'retryCount', 1);
  }

  // 네트워크 오류
  if (!error.response) {
  	return doRetry(error, axiosInstance);
  }

  return Promise.reject(error);
});

3. 요청을 보내기 위한 함수

const request = async (subUrl, data) => {
  try {
    const response = await axiosInstance.post(subUrl), data, {
      headers: {'헤더': '헤더 추가'},
    });
    return response;
  } catch (err) {
    console.log('===============원하는 형식으로 에러 컨버팅 해주기===============');
    throw error;
  }
};

위의 로직대로 요청을 보내면, axiosInstance 생성 시에 retry를 0으로 설정해주었기 때문에, socket hang up 오류가 발생했을 시에 error.config의 retryCount를 1로 설정해줍니다. 저는 lodash 라이브러리를 사용하고 있기 때문에 _.set으로 설정해주었습니다.

 

그 후 네트워크 오류 발생 시 실행되는 로직인 doRetry 함수가 실행되고, doRetry 함수 내부에서 retry를 실행한 횟수 나타내는 변수의 값을 업데이트해준 뒤 retry를 실행하게 됩니다.

 

재시도를 1번 실행하도록 했기 때문에, 1번의 재시도에서도 똑같이 실패했을 경우에는 에러 처리 로직이 실행되고, request 함수의 catch문을 타게 되어 원하는 형식으로 에러를 컨버팅 해준 후 error를 throw할 수 있게 됩니다. 저는 EXTERNAL_SERVER_ERROR로 컨버팅 해주었습니다.

 

 

 

 

 

마무리

처음 이 작업을 할당받았을 때는, interceptor를 사용하지 않고 요청하는 try-catch의 catch에서 특정 에러 시에 재시도하도록 만들었습니다. 요청을 보냈더니 끊임없이 재시도 하는 것을 보고 당황하여 선배 개발자분이 개발해두신 다른 모듈을 봤는데, interceptors라는 키워드가 있었고, axios 부분은 제가 그간 자세히 본 적이 없어 모르는 키워드였습니다.

axios interceptor retry라는 키워드로 구글링을 해보았더니 역시나 많은 정보들이 나왔습니다! 

그래도 뭔가 미리 구현해두신 같은 팀 선배 개발자 분들의 코드를 보고나서 (또는 구글링을 해서 코드를 복붙해보고나서) 해야할 일을 하는 것과, 내가 일단 똥코드라도 작성해보고나서 정답을 보는 것과는 많은 차이가 있는 것 같습니다.

정답을 보고 정답을 따라하면 3주 뒤, 6개월 뒤에 그건 제 것이 되어있지 않지만, 직접해보고 난 후에 정답을 보게 되면 오래 기억에 남게 되고 좀 더 잘 이해할 수 있게 되는 것 같습니다. 중고등학교 때 수학의 정석을 풀 때 처럼요.

axios는 제대로 보는 것은 이번이 거의 처음인데, 재밌었습니다! 아직 알아야 할 것이 많습니다! 

 

참고 링크

API Fetch Retry로직 작성해보기 (with Axios)

[Rest API] Axios로 비동기 통신하자

[axios] 라이브러리 안쓰고 custom retry하는 axios 인스턴스 만들어보기

[AXIOS] 📚 axios 설치 & 특징 & 문법 💯 정리

우리 Axios에게 다시 한 번 기회를 주세요!