장용석 블로그
4 min read
Axios 요청을 Cancel해보자 [1]

Axios 요청을 Cancel해보자

import axios from 'axios';

const source = axios.CancelToken.source();

axios.get('https://example.com', {
  cancelToken: source.token
});

source.cancel('Operation canceled by the user.');

axios 요청을 보낼 때 cancelToken을 넘겨주면 취소할 수 있다.
cancelToken을 넘기면 어디로 흘러갈까?

axios 요청을 구성하는 부분으로 타고 내려가보자…

// lib/core/Axios.js
class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }

  /**
   * Dispatch a request
   *
   * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
   * @param {?Object} config
   *
   * @returns {Promise} The Promise to be fulfilled
   */
  async request(configOrUrl, config) {
    try {
      return await this._request(configOrUrl, config);
    } catch (err) {
      if (err instanceof Error){
      ...

requst 메서드를 보면 configOrUrl, config를 인자로 받는다. 그리고 this._request 메서드를 호출한다. _request를 살펴보자.


_requset 메서드

_request(configOrUrl, config) {
  ...
  // 기본설정과 사용자 설정을 합치기도 하고...
  config = mergeConfig(this.defaults, config);
  ...
  // config에 methodt설정도 한다..  없으면 기본값으로 get을 설정하고...
  config.method = (config.method || this.defaults.method || 'get').toLowerCase();
  ...

인터셉터 세팅

인터셉터에 대한 설정도해준다.

// 인터셉터에 대한 설정도 해준다... 
const requestInterceptorChain = []; // 인터셉터 체인을 만들고
let synchronousRequestInterceptors = true; // 동기적으로 실행할지 여부를 설정한다.
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 인터셉터를 돌면서
  if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { // runWhen이 설정되어 있으면 실행한다.
    return; // false면 끝낸다.
  } 

  synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; // 동기적으로 실행할지 여부를 설정한다.

  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); // 인터셉터 체인에 fulfilled, rejected를 넣는다.
}); 

const responseInterceptorChain = []; // response 인터셉터 체인을 만들고
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { // 인터셉터를 돌면서
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); // 인터셉터 체인에 fulfilled, rejected를 넣는다.
});

requestInterceptorChain 실행

// requestInterceptorChain을 실행한다.
len = requestInterceptorChain.length; // 인터셉터 체인의 길이를 구하고

let newConfig = config; // newConfig에 config를 넣고

i = 0; // 인덱스를 0으로 설정하고 

while (i < len) { // 인덱스가 길이보다 작으면
  const onFulfilled = requestInterceptorChain[i++]; // onFulfilled에 requestInterceptorChain[i++]를 넣고
  const onRejected = requestInterceptorChain[i++]; // onRejected에 requestInterceptorChain[i++]를 넣는다.
  try { 
    newConfig = onFulfilled(newConfig); // newConfig에 onFulfilled를 실행한 결과를 넣고
  } catch (error) { 
    onRejected.call(this, error); // onRejected를 실행한다.
    break;
  }
}

dispatchRequest 실행 (실제 요청을 보내는 메서드)

// 실제 요청을 보내는 dispatchRequest를 실행한다.
// 최종 구성된 newConfig를 dispatchRequest에 넘긴다.
try {
  promise = dispatchRequest.call(this, newConfig);
} catch (error) {
  return Promise.reject(error);
}

responseInterceptorChain 실행

// responseInterceptorChain을 실행한다.
i = 0; // 인덱스를 0으로 설정하고
len = responseInterceptorChain.length; // 인터셉터 체인의 길이를 구하고

while (i < len) { // 인덱스가 길이보다 작으면
// promise에 responseInterceptorChain[i++], responseInterceptorChain[i++]를 실행한 결과를 넣는다.
  promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]); 
}

return promise; // promise를 리턴한다.
}

오오… 꽤나 단순하다. 두 인터셉터가 요청 앞뒤로 실행되는 것을 볼 수 있다.

그렇다면 이제 dispatchRequest를 살펴보자.


dispatchRequest

이 메서드 앞부분에 선언된 하나의 메서드가 있다.
throwIfCancellationRequested 라는 메서드가 dispatchRequest의 젤 앞단에서 실행된다.
이 메서드는 취소가 요청되었으면 CanceledError를 던진다. throwIfRequested에 대한 구현은 아래에 있다.

// lib/core/dispatchRequest.js
/**
 * 취소가 요청되었으면 `CanceledError`를 던진다.
 *
 * @param {Object} config  요청에 사용할 설정
 *
 * @returns {void}
 */
function throwIfCancellationRequested(config) { // 취소가 요청되었으면 `CanceledError`를 던진다.
  if (config.cancelToken) { // config에 cancelToken이 있으면
    config.cancelToken.throwIfRequested(); // throwIfRequested를 실행한다.
  }

  if (config.signal && config.signal.aborted) { // config에 signal이 있고 aborted가 true면
    throw new CanceledError(null, config); // CanceledError를 던진다.
  } 
}

/**
 * 설정된 어댑터를 사용하여 서버에 요청을 보낸다.
 *
 * @param {object} config 요청에 사용할 설정
 *
 * @returns {Promise} (이행될 Promise 객체)
 */
export default function dispatchRequest(config) {
  throwIfCancellationRequested(config); // 취소가 요청되었으면 `CanceledError`를 던진다.

더 깊이 들어가보자.

요청을 위한 준비

export default function dispatchRequest(config) {
  throwIfCancellationRequested(config); // 취소가 요청되었으면 `CanceledError`를 던진다.

  // 요청 헤더를 AxiosHeaders 객체로 변환한다.
  config.headers = AxiosHeaders.from(config.headers); 

  // 요청 데이터를 변환한다.
  config.data = transformData.call(
    config,
    config.transformRequest
  );

  // 특정 메서드에 대한 헤더를 설정...
  if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
    config.headers.setContentType('application/x-www-form-urlencoded', false);
  }
  ...

요청을 위한 설정들을 좀 해준다.

어댑터

어댑터는 config 객체에 기반해서 실제 HTTP 요청을 보내는 곳이다. 브라우저에서는 XMLHttpRequest나 fetch를 사용하고 Nodejs에서는 http 모듈 같은 것을 쓸 것이다.

getAdapter 를 통해서 적절한 어댑터를 가져온다.

const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
return adapter(config).then(function onAdapterResolution(response) {
  ...
}, function onAdapterRejection(reason) {
  ...
});

어뎁터를 통해 요청이 실행되는데,
성공하면 onAdapterResolution, 실패하면 onAdapterRejection이 실행된다.

onAdapterResolution

throwIfCancellationRequested(config);
response.data = transformData.call(config, config.transformResponse, response);
response.headers = AxiosHeaders.from(response.headers);
return response;

먼저 요청이 취소되었는지 확인하고,
응답 데이터를 변환하고,
응답 헤더를 AxiosHeaders 객체로 변환한다. 그리고 응답을 리턴한다.

onAdapterRejection

function onAdapterRejection(reason) {
  if (!isCancel(reason)) {
    throwIfCancellationRequested(config);

    // Transform response data
    if (reason && reason.response) {
      reason.response.data = transformData.call(
        config,
        config.transformResponse,
        reason.response
      );
      reason.response.headers = AxiosHeaders.from(reason.response.headers);
    }
  }

  return Promise.reject(reason);
}

요청이 실패되었다면, reason을 받아 처리한다.
오류가 취소가 아니라면, 요청이 취소되었는지 확인하고,
응답 데이터를 변환하고, (이때 다른 점은 reason.response를 넘겨준다는 것이다. 왜냐하면 요청이 실패했기 때문에 response가 없기 때문이다.)
응답 헤더를 AxiosHeaders 객체로 변환한다. 그리고 Promise.reject(reason)을 리턴한다.

이쯤 오면 대충 어디서 요청이 리턴되고 reject 되는지 알 수 있다.

이제 어댑터를 살펴보자.


Adapter (어댑터)

// lib/adapters/adapters.js
const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter
}

어댑터를 매핑도 해주고…

export default {
  getAdapter: (adapters) => {
    // ...
    return adapter;
  },
  adapters: knownAdapters
}

적절한 어댑터를 찾아 반환하고 없으면 AxiosError를 던진다.
브라우저라는 가정하에 xhrAdapter로 가보자.


xhrAdapter

// lib/adapters/xhr.js
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

export default isXHRAdapterSupported && function (config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
      ...

XMLHttpRequest 인지 확인하고, 맞으면 함수를 리턴한다.

  ...
  // XMLHttpRequest 인스턴스를 생성한다.
  let request = new XMLHttpRequest();
  ...

쭉쭉 내려가보자. (이 사이는 이것저것 요청을 위한 세팅들을 한다.) 우리가 찾고 있던 cancelToken이 어디로 흘러갔는지 보자.

    ...
    if (config.cancelToken || config.signal) {
      // Handle cancellation
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }
    ...

    request.send(requestData || null);
  });
}

오! 우리가 config에서 넣어준 cancelToken이 여기서 사용되고 있다.
자세하게 뜯어보자.

요청에 cancelToken이나 signal이 있으면, 취소 로직을 설정한다.

  if (config.cancelToken || config.signal) {
    //...
  }

onCanceled는 요청이 취소되었을 때 실행되는 함수이다.
request를 확인하고, reject을 하게 된다.
이때 cancel이 없거나 cancel.type이 있으면 CanceledError를 던지고, 아니면 cancel을 직접 던진다.
그래고 request를 abort하고, request를 null로 설정한다.

  onCanceled = cancel => {
    if (!request) {
      return;
    }
    reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
    request.abort();
    request = null;
  };

cancelToken이 있으면 onCanceled를 구독하고,
signal이 있으면 signal에 abort 이벤트를 등록한다.

  config.cancelToken && config.cancelToken.subscribe(onCanceled);
  if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }

중간에 넘어간 부분이 있는데, unsubscribe를 하는 부분이다.

  function done() {
    if (config.cancelToken) {
      config.cancelToken.unsubscribe(onCanceled);
    }

    if (config.signal) {
      config.signal.removeEventListener('abort', onCanceled);
    }
  }

요청이 끝나면 unsubscribe를 해준다.

여기까지를 통해서 직접적으로 cancelToken의 token이 요청의 config로 들어가서 어디까지 내려가고
어떻게 cancel이 구독되는지 찾아볼 수 있었다.

cancel과 거의 같이 다니는 무언가가 있다…!
signal이라는 것이다.
signal에 대해서는 다음에 또 설명해보고자 한다.

당이 떨어져서 글이 끝나갈 수록 말이 제대로 안써진다…
2편에서는 CancelToken에 대해서 알아보고,
3편에서는 signal에 대해서 알아보고자 한다.

RSS 구독