티스토리 뷰

axios는 서버와 http통신을 하기 위한 javascript기반의 라이브러리이다.

기본적으로 axios가 뭘 하는지, 왜 사용하는지 등의 기본적인 내용은 다루지 않을 예정이며

Vue로 구성된 front와 Python sanic으로 구현된 back-end의 http통신을 위한 역할을 어떻게 적용하였는지에

대해 간략히 정리해 둘 생각이다. 

 

지극히 개인적으로 필요한 용도에 맞는 기능들을 찾아 효율적으로 사용하기 위해 노력하는 편이기 때문에 전체를 다 활용하지 못하고 있을 수도 있고, 막상 효율적으로 사용하지 못하고 있을수도 있으나, 개인적으로 활용하고 있는 프로젝트에서 아직 이슈는 없다.

 

설치 및 기본적인 내용은 공식 github에서 확인 https://github.com/axios/axios

 

axios/axios

Promise based HTTP client for the browser and node.js - axios/axios

github.com

 

axios를 설치하게 되면 바로 CRUD에 사용할 수도 있지만, 반복적인 작업과, 호출하는 코드를 줄이거나 제대로 활용하기 위해서는 설정 파일 및 정형화시켜서 사용하는 것이 좋았다.

 

우선 axios.js 파일을 만들어서 적당한 위치에 둔다(나는 src/plugins 폴더 아래 두었다.

axios.js의 몇 가지 내용을 살펴보면 아래와 같다.

"use strict";

import Vue from 'vue';
import axios from "axios";
import store from '@/store/index'
import _ from 'lodash'

axios.defaults.baseURL = process.env.baseURL || '';
axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || "";
axios.defaults.timeout = 600 * 1000
axios.defaults.headers['Cache-Control'] = "no-cache,no-store,must-revalidate,max-age=-1,private";

const vue = new Vue({store})

const allowUrl = [
  '/auth',
  '/auth/refresh',    
];

let config = {
  // withCredentials: true, // Check cross-site Access-Control
  onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    vue.$store.commit('common/setProgressStatus', 'upload');
    vue.$store.commit('common/setProgress', percentCompleted);
  },

  // `onDownloadProgress` allows handling of progress events for downloads
  // browser only
  onDownloadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    vue.$store.commit('common/setProgressStatus', 'download');
    vue.$store.commit('common/setProgress', percentCompleted);
  },
};

const _axios = axios.create(config);

_axios.interceptors.request.use(
  async function (config) {
    try {
      let access_token = window.sessionStorage.getItem('access_token');

      if(!vue.$isNull(access_token)) _.merge(config.headers, {'Authorization':'Bearer '+access_token});
      else delete config.headers.Authorization;
      
      if(_.indexOf(allowUrl, config.url) == -1 && !vue.$accessToeknCheck()) {
        let returnVal = await vue.$getAccessToken();
        access_token = window.sessionStorage.getItem('access_token');

        if(returnVal && !vue.$isNull(access_token) && vue.$accessToeknCheck()){
          _.merge(config.headers, {'Authorization':'Bearer '+access_token});
          return config;
        }else{
          //vue.$awn.alert('인증토큰 획득에 실패하였습니다.');
        }
      }

      return config;
    } catch (err) {
      console.error('[_axios.interceptors.request] config : ' + err.message);
    }
  },
  function (error) {
    try {
      vue.$awn.alert('request : '+error.message);
      // Do something with request error
      return Promise.reject(error);
    } catch (err) {
      console.error('[_axios.interceptors.request] error : ' + err.message);
    }
  }
);

// Add a response interceptor
_axios.interceptors.response.use(
  function (response) {
    try {
      // Do something with response data
      return response;
    } catch (err) {
      console.error('[_axios.interceptors.response] response : ' + err.message);
    }
  },
  function (error) {
    try {
      console.log(error)
      var errorStatus = error.response.status;

      if(errorStatus == '400') vue.$awn.alert(error.response.data);
      if(errorStatus == '401') vue.$awn.alert('인증에 실패했습니다.');
      if(errorStatus == '403') vue.$awn.alert('권한이 없습니다.');
      if(errorStatus == '500') vue.$awn.alert('서버에서 오류가 발생하였습니다.');
      
      // var errorStatus = error.response.status;
      // if(errorStatus == '401')
      return error.response;
    } catch(err) {
      console.error('[_axios.interceptors.response] error : '+err.message);
    }
  }
);

Plugin.install = function (Vue) {
  Vue.axios = _axios;
  window.axios = _axios;
  Object.defineProperties(Vue.prototype, {
    axios: {
      get() {
        return _axios;
      }
    },
    $axios: {
      get() {
        return _axios;
      }
    },
  });
};

Vue.use(Plugin)

export default Plugin;

필요한 부분만 정리해서 올려놓을 수도 있지만. 전체 소스를 다 적어놓고 부분 부분 설명할 곳만 설명해주고, 나머지는 필요한 부분만 확인하고 정보를 얻어가는 분들도 있으니, 전체를 다 올려놓아야겠다. front는 Vue를 기반하여야 작성하였다.

 

"use strict";

import Vue from 'vue';
import axios from "axios";
import store from '@/store/index'
import _ from 'lodash'

필요한 라이브러리들을 import 한다. lodash는 moment와 같이 기본적이고 가장 많이 활용하는 javascript라이브러리이며 검색해보면 어떨 때 사용하는지, 왜 기본적이며 항상 사용하고 있는지를 알 수 있으며, 사용하길 추천한다.

Vue를 선언하고 객체를 생성 한 이유는 main파일에서 이미 store와 router를 포함한 Vue객체를 만들고 사용하고 있지만 여기서 store와 관련된 기능을 호출하기 위해서는 어떻게 main에서 선언된 것을 가지고 와서 사용해야 될지 몰라서 재차 선언해둔 부분이다. 찝찝하긴 하지만 나중엔 방법을 알게 되겠지 (알면 좀 댓글로..)

 

axios를 create 할 때 기본 설정들을 정의해서 생성할 수 있는데 많은 것을 설정할 수 있지만 필요하거나 아는 것만 넣는 편이라 onUploadProgress, onDownloadProgress만 선언해서 생성하였다.

let config = {
  // withCredentials: true, // Check cross-site Access-Control
  onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    vue.$store.commit('common/setProgressStatus', 'upload');
    vue.$store.commit('common/setProgress', percentCompleted);
  },

  // `onDownloadProgress` allows handling of progress events for downloads
  // browser only
  onDownloadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    vue.$store.commit('common/setProgressStatus', 'download');
    vue.$store.commit('common/setProgress', percentCompleted);
  },
};

const _axios = axios.create(config);

onUploadProgresss의 경우에는 axios를 통해 http통신을 할 경우 데이터나 정보를 업로드할 때 실시간 event를 컨트롤하기 위한 부분이다. 주로 파일 업로드 시 업로드 %를 나타낼 때 사용하기도 하지만, 나는 모든 경우에 다 적용되게 만들고 진행상태 %를 가져와 화면 상단에 얇게 들어간 로딩바에 적용을 해 두었다. 대부분 눈 깜짝할 사이에 처리되기 때문에 잘 티가 나지는 않지만, 조금이라도 시간이 걸릴 경우 상단에 진행 상태가 잠깐이나마 나타내기 위한 기능이다.

사실 필수적 요소는 아니기 때문에 기능이 궁금해서 한번 적용해 본 것이다. onDownloadProgress는 반대로 파일이나 데이터 다운로드 시 사용하는 이벤트이다, 

 

 

다음 설명할 부분은 axios에서 제공하는 interceptors 부분인데 request와 response로 나눠서 설정할 수 있다.

_axios.interceptors.request.use(
  async function (config) {
    try {
      let access_token = window.sessionStorage.getItem('access_token');

      if(!vue.$isNull(access_token)) _.merge(config.headers, {'Authorization':'Bearer '+access_token});
      else delete config.headers.Authorization;
      
      if(_.indexOf(allowUrl, config.url) == -1 && !vue.$accessToeknCheck()) {
        let returnVal = await vue.$getAccessToken();
        access_token = window.sessionStorage.getItem('access_token');

        if(returnVal && !vue.$isNull(access_token) && vue.$accessToeknCheck()){
          _.merge(config.headers, {'Authorization':'Bearer '+access_token});
          return config;
        }else{
          //vue.$awn.alert('인증토큰 획득에 실패하였습니다.');
        }
      }

      return config;
    } catch (err) {
      console.error('[_axios.interceptors.request] config : ' + err.message);
    }
  },
  function (error) {
    try {
      vue.$awn.alert('request : '+error.message);
      // Do something with request error
      return Promise.reject(error);
    } catch (err) {
      console.error('[_axios.interceptors.request] error : ' + err.message);
    }
  }
);

// Add a response interceptor
_axios.interceptors.response.use(
  function (response) {
    try {
      // Do something with response data
      return response;
    } catch (err) {
      console.error('[_axios.interceptors.response] response : ' + err.message);
    }
  },
  function (error) {
    try {
      console.log(error)
      var errorStatus = error.response.status;

      if(errorStatus == '400') vue.$awn.alert(error.response.data);
      if(errorStatus == '401') vue.$awn.alert('인증에 실패했습니다.');
      if(errorStatus == '403') vue.$awn.alert('권한이 없습니다.');
      if(errorStatus == '500') vue.$awn.alert('서버에서 오류가 발생하였습니다.');
      
      // var errorStatus = error.response.status;
      // if(errorStatus == '401')
      return error.response;
    } catch(err) {
      console.error('[_axios.interceptors.response] error : '+err.message);
    }
  }
);

request의 경우 axios를 통해 http통신 시 기입한 url에 가기 전에 거쳐간다고 보면 되고 response는  url에 다녀와서 호출한 지점의 callback에 도착하기 전에 거쳐간다고 보면 된다. 즉 가기 전과 오기 직전 공통적으로 해야 할 처리가 있다면 유용하게 사용될 수 있다.

 

내가 만든 프레임워크는 back-end와 API 기반의 통신을 하며 별도의 session관리를 통해 사용자를 관리하지 않기 때문에 JWT Token을 이용하여 사용자의 인증이나, API의 데이터 획득 권한을 확인한다. 

request에서는 화면에서 가지고 있는 access_token이 유효시간이 아직 남았는지, 정상적인지 등의 판단하며 기간이 초과되었거나 이상이 있을 경우 refresh_token을 이용해 access_token을 재발급받아서 request를 계속 진행하는 로직이 적용되어 있다. 물론, 자세한 내용들은 다 별도로 작성되어 있다.

 

반대로 response에서는 error발생에 대한 처리를 해주고 있다. back-end에서 문제가 발생할 경우 error코드를 대부분 다 명시하여 보내주도록 작성해 뒀기 때문에 error status에 따른 toast메시지를 표시하는데 활용하고 있다.

 

이렇게 설정 파일에서는 간단 히 설정 작업들을 할 수 있으며, 이제 CRUD를 어떻게 효율적으로 할 수 있을지 확인해보자

 

 

실제 화면에 작성되어있는 Vue파일에서 get method를 통해 데이터를 요청할 경우에 아래와 같이 작성되어 있다.

let apiSet = new this.$Apiset({
  data: {
    method: "GET",
    url: "/api/system/users",
    formData: {},
    queryParam: this.params,
    btnObj: this.$refs.mainSearchBtn,
    gridOptions: this.mainGridset.gridOptions,
  }
});
let returnVal = await apiSet.send();

// 조회 후 통신 상태가 200(성공)일 경우
if(returnVal.status == '200'){
  // 데이터 셋에 데이터 입력
  await this.mainGridset.setData(returnVal.data);
  if(this.mainGridset.rowcount > 0) {
    this.mainGridset.rowposition = 0;
  }
}

이게 뭐지? axios는 사용 중인 게 맞나? 등의 의문을 가질 수 있지만, 당연하다고 생각된다. 

$Apiset이란 건 별도로 http통신을 axios를 통해 할 때 편하게 기본적인 설정이나 반복적인 작업들을 줄이기 위해 만든 vue extend이다. data내의 내용을 보면 method, url, formData, queryParam, btnObj, gridOptions 등의 내용이 있는데 그냥 항상 복사해서 붙여 넣으며 사용한다. 

 

복사해서 붙여 넣고 method와 url만 변경해서 주로 사용한다. queryParam은 왜 항상 동일할 수 있지?라는 생각이 들 수 있는데 메인 조회 시 검색조건의 경우 queryParam에 지정해 두는데 내가 작성한 대부분의 화면에 검색조건은 vue에서 아래와 같이 선언하고 사용 및 바인딩되어있기 때문에 모든 화면에 동일하게 조회 로직을 처리할 수 있다.

// 검색조건 관련
params: {
  sUserGb: '100', 
  sUserIdNm: null, 
  sUseYn: 'Y',
  sIsAdmin: this.isAdmin,
},

보통 get method의 경우 queryParam으로 post, put 등은 formData에 JSON형태의 데이터를 담아 전송하는데 그냥 그렇게 쓰는구나 정도로 생각하고 쓰다가 시간이 남으면 한 번 찾아보는 것도 좋다.

 

btnObj는 해당 호출이 조회 버튼을 눌리게 되면 동작하는 기능이라면 조회 버튼 객체를 함께 지정하면 조회 버튼이 해당 기능이 동작할 동안에는 빙글빙글 도는 Loading 아이콘이 나타나도록 편하게 설정하기 위해 공통으로 처리하는 부분이다.  gridOptions도 조회된 데이터의 내용이 목록에 다 나타나기 전까지의 로딩 옵션 등을 지정하기 위한 부분이다. (한 마디로.. 지극히 개인적인 내 프레임워크에만 사용되는 내용이니 이렇게 쓰는 사람도 있구나.. 정도)

 

let returnVal = await apiSet.send();

위 부분에서 http통신 호출을 하게 되며 send호출 시 따라가 보면 대략..

send: function() {
  try {
    var methodUpper = this.method.toUpperCase();
    var returnData = '';

    if(methodUpper == 'GET'){
      returnData = vue._$get(this);
    }else if(methodUpper == 'POST'){
      returnData = vue._$post(this);
    }else if(methodUpper == 'PUT'){
      returnData = vue._$put(this);
    }else if(methodUpper == 'DELETE'){
      returnData = vue._$delete(this);
    }

    return returnData;
  } catch (err) {
    console.error("apiset.vue > methods > send error :" + err);
  }
},

이런 내용이며 또 저기서 vue._$get(this).. 는 뭐며 왜 또 없는 건데 라고 답답해하실까 봐 바로 따라가 보면 

Vue.prototype._$get = async function (pApiset) {
  try {
    let returnInfo = '';
    this._$btnLoadingShow(pApiset.btnObj);
    this._$gridLoadingShow(pApiset.gridOptions);

    returnInfo = await window.axios.get(
      pApiset.url,
      {
        params: pApiset.queryParam,
        headers: pApiset.header,
        responseType: pApiset.responseType ? pApiset.responseType : ''
      }
    );

    return returnInfo;
  } catch (err) {
    console.log('function_api $get() Error : ' + err.message);
  } finally {
    this._$btnLoadingHide(pApiset.btnObj);
    this._$gridLoadingHide(pApiset.gridOptions);
  }
}

Vue.prototype._$post = async function (pApiset) {
  try {
    let returnInfo = '';
    this._$btnLoadingShow(pApiset.btnObj);
    this._$gridLoadingShow(pApiset.gridOptions);

    returnInfo = await window.axios.post(
      pApiset.url,
      pApiset.formData,
      {
        params: pApiset.queryParam,
        headers: pApiset.header,
      }
    );

    return returnInfo;
  } catch (err) {
    console.log('function_api $post() Error : ' + err.message);
  } finally {
    this._$btnLoadingHide(pApiset.btnObj);
    this._$gridLoadingHide(pApiset.gridOptions);
  }
}

기본적인 내용이 급하면 여기부터..

 

대략 저런 내용들이 있다. Header가 빠져있으면 Header도 내가 지정한 Header로 설정도 해주고.. 사실 별로 하는 게 없어 보이지만 나중에 혹시 모를 공통 작업에 화면 파일을 200개를 만들면 200개의 화면에 작업해주기가 너무 귀찮아서 모으고 모으고 모은 거라고 생각하면 된다. 사실 axios의 crud에 필요한 호출 부분은 여기라서 여기만 봐도 될 것 같긴 하다.

(pApiset은 무시하고 보면 더 편하다. pApiset.url -> url , pAipset.queryParam -> queryParam)

window.axios.get(
  pApiset.url,
  {
    params: pApiset.queryParam,
    headers: pApiset.header,
    responseType: pApiset.responseType ? pApiset.responseType : ''
  }
);

window.axios.post(
  pApiset.url,
  pApiset.formData,
  {
    params: pApiset.queryParam,
    headers: pApiset.header,
  }
);

window.axios.put(
  pApiset.url,
  pApiset.formData,
  {
    params: pApiset.queryParam,
    headers: pApiset.header,
  }
);

window.axios.delete(
  pApiset.url,
  pApiset.formData,
  {
    params: pApiset.queryParam,
    headers: pApiset.header,
  }
);

그냥 이 부분? 이렇게 호출하면 promise형태의 응답을 반환하는데 await를 이용하거나. then방식을 이용해 응답이 오고 난 뒤 처리를 해주게 되면 응답 이후 응답 값에 따른 동작을 처리할 수 있다.

//await를 이용해서 값을 받아서 처리하거나
returnInfo = await window.axios.delete(
  pApiset.url,
  pApiset.formData,
  {
    params: pApiset.queryParam,
    headers: pApiset.header,
  }
);

console.log(returnInfo.data)

//then을 이용해서 처리 
returnVal.then(function(response){
  // 조회 후 통신 상태가 200(성공)일 경우
  if(response.status == '200'){
    self.mainGridset.deleteRow(rowposition);
    self.$awn.success('삭제가 완료되었습니다.');
  }else {
    self.$awn.alert('삭제에 실패하였습니다.');
  }
})

 

댓글로 물어보신 분들도 있고, 사실 예전만큼 정성을 가득 쏟으면서 작성하기는 너무 귀찮음이 커서 만들어 둔 소스에서 복사 붙여 넣기 하며 설명도 어느 정도 이해 수준이 있다고 생각하고 작성한 글이라, 알아보기 어려울 수 있으나,

정말 axios기본 기능은 axios github에 더 잘 나와있고, 프레임 워크에 어떤 식으로 적용하였느냐에 대해 이렇게 쓰는 경우도 있구나 정도로 생각해서 보는 게 좋을 것 같다. 

 

그리고 혹시나 JWT나 Apiset 등 개별로 작성한 모듈들이 궁금하거나 필요한 경우나 급히 작성한 글에 대해서 궁금하거나 잘못된 부분들이 있다면 댓글 작성해두시면 글을 고쳐나가면서 더 좋은 글이 되길 한번 기대합니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함