
시작하기 전에
API 요청을 위해 fetch 함수를 사용하다가 네트워크 응답으로 **“body must be object”**라는 에러를 마주치게 되었다.
예전에 axios를 사용했을 때는 몰랐는데, fetch를 사용할 때는 header 옵션을 직접 설정해줘야 한다는 것을 알게 됐고,
fetch는 왜 수동으로 직렬화 해야하며, axios는 왜 직접 설정할 필요가 없는지 코드 분석을 통해 비교해보고자 글을 쓰려고 한다.
자바스크립트 객체는 네트워크 전송이 안된다?


로그인 진행 시에 email, password가 필요하여 그대로 요청을 보냈는데, 응답으로 에러메시지가 나왔다.
구글링 및 API 문서를 확인하다보니 fetch 사용 시 요청 데이터는 json 형태이므로 headers에 다음과 같이 명시해줘야 한다는 것이다.
headers: {"Content-Type": "application/json"}
당장 alert({name:'철수',age:30}) 만 콘솔에 쳐서 확인해보면

[object Object] 라는 요상한(?) 객체가 나온다.
추측해보건대, 이렇게 객체가 변환되기 때문에 json 객체로 수동으로 변환해야 하는 것 같다.
[object Object]?
[object Object]로 변환되는 원리를 파악하기 위해 클로드와 함께 소스코드를 까보기로 했다.
ECMAScript를 살펴봤을 때

출처 : https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.prototype.tostring
위 스펙에 따라 일반 객체는 [object Object]라는 형태로 나오게 되는 것을 알 수 있다.
또한 v8 소스코드를 살펴보면 Torque라는 언어로 내장 함수를 구현했다고 한다.
transitioning javascript builtin ObjectPrototypeToString(
js-implicit context: Context, receiver: JSAny)(): String {
return ObjectToString(context, receiver);
}
왜 일반 자바스크립트 객체는 네트워크 전송이 되지 않을까?
데이터를 네트워크로 보낼 때 왜 JSON 객체로 래핑해서 보내야 할까?
우선 네트워크는 텍스트 또는 바이트 스트림만 허용하고 자바스크립트 같이 자바스크립트 런타임에만 허용되는 객체는 보안상 허용하지 않는다고 한다.
이를 전송 가능한 객체로 변환하기 위해 직렬화 하는 과정이 필요하다.
const jsonString = JSON.stringify(user);
// '{"name":"김철수","age":25,"friends":["영희","민수"]}'
그렇다면 fetch 함수를 통해 데이터를 JSON.stringify를 감싸면 되냐? 그게 끝이 아니다.
headers 객체를 통해 Content-type을 명시해주지 않으면 앞서 나왔던
{
"success": false,
"error": {
"message": "body must be object",
"statusCode": 400
}
}
위 에러가 나오게 된다.
const response = await fetch(
`${import.meta.env.VITE_API_URL}/api/auth/login`,
{
method: "POST",
body: JSON.stringify(data),
// headers 설정 필요!
headers: {
"Content-Type": "application/json",
},
},
);
Axios
이에 반해 axios는 fetch와 달리 데이터를 자동으로 직렬화 해준다고 하는데, 정말 그런지 알아보기로 했다.
// https://github.com/axios/axios/blob/v1.x/lib/defaults/index.js
function stringifySafely(rawValue, parser, encoder) {
if (utils.isString(rawValue)) {
try {
(parser || JSON.parse)(rawValue);
return utils.trim(rawValue);
} catch (e) {
if (e.name !== 'SyntaxError') {
throw e;
}
}
}
return (encoder || JSON.stringify)(rawValue);
}
const defaults = {
transitional: transitionalDefaults,
adapter: ['xhr', 'http', 'fetch'],
transformRequest: [function transformRequest(data, headers) {
const contentType = headers.getContentType() || '';
const hasJSONContentType = contentType.indexOf('application/json') > -1;
const isObjectPayload = utils.isObject(data);
if (isObjectPayload && utils.isHTMLForm(data)) {
data = new FormData(data);
}
const isFormData = utils.isFormData(data);
if (isFormData) {
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}
if (utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data) ||
utils.isReadableStream(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
headers.setContentType('application/x-www-form-urlencoded;charset=utf-8', false);
return data.toString();
}
let isFileList;
if (isObjectPayload) {
if (contentType.indexOf('application/x-www-form-urlencoded') > -1) {
return toURLEncodedForm(data, this.formSerializer).toString();
}
if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
const _FormData = this.env && this.env.FormData;
return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
}
}
if (isObjectPayload || hasJSONContentType ) {
headers.setContentType('application/json', false);
return stringifySafely(data);
}
return data;
}]
// https://github.com/axios/axios/blob/v1.x/lib/utils.js#L113
const isObject = (thing) => thing !== null && typeof thing === 'object';
해당 코드를 살펴보면 axios는 transformRequest를 통해 데이터를 자동으로 직렬화하고 headers 자동으로 설정한다.
예시 코드로 비교
fetch
const response = await fetch(
`${import.meta.env.VITE_API_URL}/api/auth/login`,
{
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
},
);
axios
const res = await axios.post<LoginResponse>(
`${import.meta.env.VITE_API_URL}/api/auth/login`,
data
);
마무리
그동안 fetch와 axios를 단순히 API 요청 툴로만 활용하고 파볼 생각은 안하고 있었는데, 이번 에러를 만나게 됨으로써 해당 툴들이 어떻게 동작하는지 대략적으로나마 알게 되었고, 다음에는 fetch와 axios의 각 기능 및 장단점을 비교해 볼 예정이다.



