Santos의 개발블로그

Axios 뜯어보기 본문

NPM/Valuable

Axios 뜯어보기

Santos 2023. 11. 30. 23:43

 

* 이 글은 Axios 내부 동작을 살펴 보면서 느꼈던 점들을 기록하였습니다. 

 

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

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

github.com

사내의 같은 팀(프론트엔드 팀)에서 진행된 오픈소스 파보기 스터디를 참여하였다. "오픈소스의 기여자는 아니더라도 한 번쯤은 읽어봐야지"라는 다짐을 오랫동안 미뤘던 터라 이번 스터디는 더욱 의미가 있었다.

 

오픈소스 파보기 스터디는 아래와 같은 순서로 진행되었다.

 

1. 자신이 궁금했던 오픈소스 1가지를 정한다. 방대한 것보다는 작지만 알찬 것을 선택하는 것이 좋다. 

2. 멘토가 전달한 TIP을 기억하며, 조급한 마음과 두려운 마음을 버리고, 확인해 볼 수 있는 것들을 하나씩 짚어간다. 

3. 오픈소스 탐방 중 길을 잃거나 예상치 못한 변수를 만났을 때 멘토와 상의를 꼭 한다. 

4. 본인이 탐방한 오픈소스를 간략하게 정리하여 다른 팀원들에게 발표한다. 

 

 물론 전에도 오픈소스 탐방에 맛을 보려고 노력은 했으나, 괜한 상상력에 사로잡혀 미루는 것이 부지기수였다. 그러나 멘토의 조언을 따라 하나씩 차근차근 살펴보는 연습을 꾸준하게 하다 보니, 각 소스의 세부적인 독해보다 코드의 흐름을 따라 각 함수의 역할을 파악하고, 예상해 보는 일련의 과정들이 "어렵다"라는 고지식한 생각의 금이 가기 시작하였다.

 

 내가 선택한 오픈소스는 대다수 프로젝트에서 쓰이는 라이브러리 중 하나인 Axios다. 아래와 같은 궁금증을 가지며 탐방을 시작하였다.

 

1. 어떻게 Axios는 모든 브라우저 지원이 가능할까?

2. Promise를 어떤 방식으로 구현하였을까?

3. Interceptor 구조와 flow는 어떠할까?

 

위 3가지 궁금증과 함께 Axios 탐방하기 시작해 보려 한다. 


구조

모든 소스를 살펴보지 않고, 아래의 3가지 파일을 뜯어보았다. ( axios.js 와 Axios.js는 다른 파일입니다.)

 

1. axios.js

2. InterceptorManager.js

3. Axios.js

 

관심사를 분리된 3가지 파일은 모두 클래스로 작성되었으며, 커다란 구조는 다음과 같다.

 

Axios 클래스 생성자에서는 InterceptorManager 인스턴스를 각각 만들어 request와 response 속성값으로 할당하고, axios.js파일에서는 createInstance 함수 내부에 Axios 인스턴스 화를 하는 구조로 되어 있다.


코드

1. axios.js

axios.js 파일 내부는 그리 복잡하지 않다. 여기서는 만든 인스턴스를 User가 사용할 수 있도록 제공해 주는 역할을 한다. 그렇기에 인스턴스를 만드는 createInstance 함수에 집중하면 된다. 아래의 코드를 보자.

createInstance 함수

createInstance 함수

createInstance 함수 내부에서 중요한 역할을 하는 함수는 2가지이다. 

1. Axios.prototype.request와 context를 apply 하는 bind 함수
2. axios.prototype을 instance에 복사하고, context를 instance에 복사하는 extend 함수
bind 함수
export default function bind(fn, thisArg) {
	return function wrap() {
		return fn.apply(thisArg, arguments);
	};
}

 

bind 함수의 내부를 살펴보면 익명함수(wrap)를 함수 내부에 랩퍼 형식으로 둘러쌓여 반환한다. "굳이 이럴 필요가 있을까?" 하는 의문을 가지고 Issue 를 뒤져보다 보니 아래와 같은 글을 찾을 수가 있었다. 

wrap 함수 존재 여부 질문에 답변은 ES3에서 부터 쓰여 남아있는 Polyfill 이라는 것이었고 곧 지워질 예정이라고 하였다. (물론 언제 지워질지는 모르겠지만..) 

다시 본론으로 돌아와 Axios 인스턴스인 context를 Axios.prototype.request에 apply를 하는 간단한 함수이다. 

extend 함수
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
    });
  return a;
}

 

다음으로 살펴볼 extend 함수는 for 문을 돌면서 b 인자들을 a의 메서드로 복사하는 함수이다. 자세히 보면 여기서도 위에 언급한 bind 함수를 사용하여 둘의 인자를 연결하는 모습을 볼 수 있다. 

 

함수인 경우는 apply를 사용하여 연결하고, 속성값인 경우는 해당 key에 알맞은 값에 할당 한다. 

createInstance 호출

 

이렇게 만들어진 createInstance 함수를 바로 아래에서 호출한다. 호출 시 defaults 인자를 넣어주는데, 여기서 defaluts의 값은 axios Readme 에 있는 기본 axiosConfig 설정값이다.

여기서 반환된 값은 bind와 extend 된 Axios의 인스턴스이고, 바로 아래 반환된 axios 인스턴스에 Axios 클래스를 속성값으로 할당하면서 create 함수가 아니더라도 axios가 가지고 있는 메서드를 쓸 수 있도록 하였다.

결과값

 

axios.CancelToken = CancelToken과 같이 axios 인스턴스의 속성값으로 할당하는 코드들로 나열이 되어 마지막에는 axios를 export 한다. 이렇게 만들어진 axios를 console 창에서 찍어보았을 때 위와 같은 결과값을 얻을 수 있다.

 

여기서 주목할 부분은 wrap으로 감싸져 있는 Axios 프로토타입이었고,  axios 인스턴스의 속성값으로 할당을 한 부분은 궁금한 부분으로 남았다. 의도가 불분명한 코드는 아니라고 생각되어, 시간이 날 때 찾아보려고 한다. (찾는 즉시 블로그에 올릴께요....🙃) 


InterceptorManager.js

해당 코드를 살펴보기 전에 호출되는 구현부를 살펴보는 것이 먼저라고 생각되었다. 구조에서 살펴보았듯이 Axios 클래스의 생성자 함수에서 호출을 하고 있엇다. 

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

 

interceptors 객체 안에 request와 response의 키를 갖게 하고, 값에는 InterceptorManager 인스턴스가 할당된 것을 볼 수 있다. 해당 기능을 사용하는 쪽에서 생각해 보면 구조를 이해하기가 더 쉬운데, "왜 interceptors.reqeust, interceptors.response 내부에 있는 함수를 호출했는지" 인지할 수 있다. 

interceptors 사용하는 쪽

 

use 함수를 사용하고 callback 함수를 인자값으로 할당하는 것으로 보아 내부 구현이 어느 정도 유추가 가능하다. 본론으로 돌아와 InterceptorManager 클래스 내부를 하나씩 살펴보자. 

 

사실 내부에 복잡하거나 읽기가 어려운 코드는 존재하지 않는다. 예상했다시피 Stack 구조를 구현하기 위한 배열 handlers가 생성자에 할당 되어있고, 값을 stack에 쌓는 use 메서드와 pop을 할 수 있는 eject 메서드, 그리고 handlers 배열을 초기화하는 clear 메서드, 마지막으로 handlers를 순회하는 forEach 메서드가 존재한다. 

 

해당 메서드 각각의 역할을 알아보자.

use
use(fulfilled, rejected, options) {
	this.handlers.push({
		fulfilled,
		rejected,
		synchronous: options ? options.synchronous : false,
		runWhen: options ? options.runWhen : null
	});
	return this.handlers.length - 1;
}

fullfilled, rejected 함수를 인자 값으로 받아 handlers 배열에 push 하는 역할을 한다. 배열에 추가된 함수들은 이후 설명할 forEach 내부에서 순회를 통해 호출된다. 

eject 
eject(id) {
	if (this.handlers[id]) {
		this.handlers[id] = null;
	}
}

앞서 설명했듯이 id 값을 인자로 받아 해당 id값이 handlers 배열에 존재할 경우 할당된 값에 null을 할당하는 함수이다. 

forEach
forEach(fn) {
	utils.forEach(this.handlers, function forEachHandler(h) {
		if (h !== null) {
			fn(h);
		}
	});
}

함수를 인자로 받아 handlers 배열을 순회하면서 값을 하나씩 꺼내어 인자값으로 할당한다. 이때 인자로 받은 함수는 호출된다. 

사실 이렇게만 보면 잘 이해가 되지 않는다. 단지 단일 책임 원칙을 잘 따르고 있는 함수의 불과하기 때문이다. 

이를 더 쉽게 이해하기 위해서는 해당 메서드들을 호출하는 구현 부를 살펴보면 된다. 메서드를 사용하는 Axios.js 파일로 넘어가 중추 역할을 담당하는 request 함수를 살펴보자.


Axios.js

직접 코드를 살펴보면 3가지 파일(axios.js, intercepterManager.js, Axios.js) 중 가장 길지만, 핵심만 뽑아 살펴보았을 때 생각보다 명료하였다. 일단 아래 3가지 Chain 배열을 기억한채로 코드를 살펴보자.

const requestInterceptorChain = [];
const responseInterceptorChain = [];
const chain = [dispatchRequest.bind(this), undefined];

 

3가지 Chain 변수들은 request interceptor, reqeust, responce interceptor 은 각자의 역할을 한다. 3종류의 배열은 체인이라는 변수명처럼 아래와 같이 하나의 체인으로 엮인다.

// requestInterceptorChain
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
	(생략....)

	requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

// responseInterceptorChain
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
	(생략....)
	
	responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

// chain
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);

 

forEach 함수의 쓰임새

 

여기서 앞서 언급했던 InterceptorManager 클래스의 forEach 메서드가 쓰이는 걸 볼 수있다. forEach 함수 인자로 callback을 넘겨주고 forEach에서는 handlers 스택을 돌면서 인자로 들어온 callback 함수에 값을 넘기면서 호출한다. 

이후 interceptor 인자로 들어온 객체 값 중 fulfilled와 rejected 메서드를 requestInterceptorChain 배열에 차례로 담는다.

 

responseInterceptorChain도 requestInterceptorChain과 마찬가지므로 설명은 생략하려고 한다. 

이후 호출이 될 순서대로 하나의 체인을 만드는 데 해당 결과는 아래와 같다.

// result1: interceptor 체인들끼리 묶였을 때 
[fulfilled1, rejected1, fulfilled2, reject2]

// result2: interceptor + request 체인까지 모두 묶였을 때
[...requestChain, dispatchRequest, ...responseChain]

 

fulilled1, rejected1 함수는 request interceptor에서 쓰이는 성공 및 실패에 대한 함수가 chain에 앞쪽, fulilled2, rejected2 함수는 response interceptor에서 쓰이는 성공 및 실패에 대한 함수는 chain의 뒤쪽, 마지막으로 request 담당할 함수가 가운데 위치할 수 있도록 한다.

let promise;
let len = chain.length;

promise = Promise.resolve(config);
	while (i < len) {
		promise = promise.then(chain[i++], chain[i++]);
	}
return promise;

 

만들어진 chain은 config 설정 값과 함께 promise chain으로 converting을 한다. 

단어의 뜻대로 interceptor는 가로채다, 여기서는 API의 request를 가로채기 때문에 본래의 호출보다 먼저 request interceptor는 먼저 실행된다. 아래의 코드에서 살펴보자.

len = requestInterceptorChain.length;
let newConfig = config;
i = 0;

while (i < len) {
	const onFulfilled = requestInterceptorChain[i++];
	const onRejected = requestInterceptorChain[i++];

	try {
		newConfig = onFulfilled(newConfig); ------> 유저가 사용하는 interceptors
	} catch (error) {
		onRejected.call(this, error); ------> 유저가 사용하는 interceptors
		break;
	}
}

 

여기서는 promise chain은 신경쓰지 말자. requestInterceptorChain의 길이만큼 while문을 돌면서 fulfilled 함수와 onRejected 함수를 꺼낸다. 그리고 try, catch 문에서 호출을 하는데 error가 생긴다면 onRejected 함수를 호출하고, 아니라면 onFuilled함수를 호출한다.

이때 newConfig를 인자로 넣어주는데 그 인자값이 바로 우리가 interceptor 를 호출했을 때 볼 수 있는 config 값이다. (아래 사진) config 내부에는 사용자가 정의한 값과 Axios가 정의한 값이 병합 되어 있다. 

interceptors 사용하는 쪽

간단하게 정리하자면 request.use 를 호출 할 때 인자로 넘겨준 callback 함수가 InterCeptorManager가 관리하고 있는 handlers 스택에 쌓이고, request가 이루어졌을 때, requestInterceptorChain에 담겨 호출되는 것이다. 그렇다면 request는 언제 호출 될까?

다음 코드를 보자. 

try {
	promise = dispatchRequest.call(this, newConfig);
} catch (error) {
	return Promise.reject(error);
}

 

드디어 request를 호출한다. 여기서 dispatchRequest는 함수이다. (dispatchRequest.js 파일에 있다...). dispatchRequest 함수를 한번 살펴보자.

export default function dispatchRequest(config) {

(생략....)

const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData.call(
      config,
      config.transformResponse,
      response
    );

    response.headers = AxiosHeaders.from(response.headers);

    return response;
  }, 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);
  });
};

 

음.. 조금 복잡스러워 보이지만, 필요한 부분만 체크하면서 나눠서 읽어보자. 위에서 dispatchRequest 함수를 호출할 때 config 인자 값을 넘겨주었다. 이때 getAdapter함수를 호출한다. 세부 코드를 살펴보면 config에 정의한 (또는 사용자가 정의한) http or xhr를 사용하여 promise 타입의 adapter를 하나 만들어 반환하다. 

const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

 

이후 adapter 함수를 호출하고 결과인 response 값을 다시 then 이후 인자 값으로 넣어준다.

then이후에 들어온 response값은 transformData 함수를 호출하여 값을 알맞게 변형한 뒤 다시 반환한다. dispatchRequest 함수 호출에 대한 반환된 값은 promise 변수에 할당된다. 

 

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData.call(
      config,
      config.transformResponse,
      response
    );

    response.headers = AxiosHeaders.from(response.headers);

    return response;
  }, 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);
  });

 

이후에 진행되는 코드는 response interceptor 쪽과 연결되어 있다. 이제 거의 다 왔다. 마지막 코드를 살펴보자.

현재 promise 값에는 dispatchRequest 함수의 반환 값이 들어 있다. 

 i = 0;
 len = responseInterceptorChain.length;

 while (i < len) {
    promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
 }

 

requestInterceptorChain 코드와 별다른 부분은 없다. 허나 여기서 중요하게 살펴보아야 할 부분은 promise.then 구문이다. promise.then 호출을 할 때 responseInterceptorChain 의 fullfilled 함수와 rejected함수를 인자 값으로 건네준다.

interceptors 사용하는 쪽

requestInterceptorChain호출을 했을 때 보았던 사진이다. promise는  dispatchRequest에서 호출되어 반환된 response 값을 가지고 있다가 사용자가  response.use 를 호출 할 때 인자로 넘겨준 callback 함수의  인자로 넣어준다. 여기서 response의 값은 reqeust에 대한 결과 값이다.  마지막에는 다시 response값을 반환된다.

 

반환된 response 값은 사용자에게 다시 반환되면서 전체적인 Axios 함수의 흐름은 끝이 난다. 

return promise;

 

반환된 promise 값은 잘 알다시피 우리들의 요청에 대한 responce 값이다. 


결론

 내가 가졌던 궁금증은 아래와 같았다.

 

1. 어떻게 Axios는 모든 브라우저 지원이 가능할까?

2. Promise를 어떤 방식으로 구현 하였을까?

3. Interceptor 구조와 flow는 어떠할까?

 

"Axios 라이브러리를 살펴보기 전 궁금증의 대한 해결이 잘 이루어 졌을까?" 에 대한 대답은 "어느정도" 인 것 같다.

100% 온전하게 가려운 부분을 긁어주지는 못했지만,  Interceptor 구조와 Axios 코어에 있는 구조들이 어떻게 유기적으로 연결되어 있고, 흘러가는지 파악할 수 있었던 것 같다. 

 신기했던 부분은 생각보다 테스트가 잘 짜여져 있었고,  cjs와 esm을 모두 지원하는 type들이 각각 적혀져 있어 섬세한 라이브라리라는 느낌을 받았다. 특히 테스트는 현재 회사에서 진행하고 있는 프로젝트에서도 충분히 참고할 수 있는 부분이라 생각했다.

 

어렵기보다는 두려워서 실행해 옮기지 못했던 내게 조금은 위안이 되었던 스터디였다. 함께한 스터디 조원들에게 감사하다는 말을 남기며 글을 마치려고 한다.  


< 참고자료 >

 

[사이트] #Axios (github 저장소)

 

https://github.dev/axios/axios

Setting up your web editor eyJzZXJ2ZXJDb3JyZWxhdGlvbklkIjoiNDRhOWUwN2UtN2ZlOS00YTUwLTlhODMtNmJkOGQ0MWE3MzI4Iiwid29ya2JlbmNoVHlwZSI6ImVkaXRvciIsIndvcmtiZW5jaENvbmZpZyI6eyJ2c2NvZGVWZXJzaW9uSW5mbyI6eyJpbnNpZGVyIjp7ImNvbW1pdCI6ImM2YjQ4YzM2NGRkZTcwYzEyYmY5NzdmM

github.dev

 

<Valuable> Axios 탐방기 end

'NPM > Valuable' 카테고리의 다른 글

[React-query] 데이터 filtering 하기  (0) 2024.03.02
Recoil  (0) 2023.11.04
cookie  (0) 2020.12.26
cookie-parser  (0) 2020.08.01
axios  (0) 2020.07.26
Comments