엘리스 AI 트랙에서 3차 프로젝트를 진행하면서 내가 제일 처음 했던 것은 api 요청을 위한 틀이 되어줄 함수들을 모아놓은 api.ts를 만드는 일이었다. 여태 해왔던 것처럼 axios를 사용했고 코드 작성을 마친 뒤 프로젝트 코치님께 코드 리뷰를 요청드렸을 때 제일 처음 받았던 피드백은, axios를 쓴 데에 특별한 이유가 있나요? 였다.
그때 내가 했던 답변은 그냥 axios를 지금까지 써봤고 fetch가 낯설다는 것이 끝이었다. 그렇게 스스로 나는 왜 axios를 썼지? 라는 질문의 늪에 빠졌고 고민 끝에 다음의 이유를 들어 axios로 작성된 모든 코드를 fetch로 변경했다.
이렇게 보니 axios를 쓸 필요가 있나? 싶어 그 후 진행하는 프로젝트에도 fetch를 사용 중이지만 다른 사람들의 코드를 보면 여전히 axios는 건재하다.
그래서 이번 글을 통해 axios와 fetch를 비교하고 어떤 것을 선택해야 하는지 알아보고자 한다.
Fetch API는 fetch()
메서드를 통해 HTTP 요청과 응답을 JavaScript에서 접근, 조작할 수 있는 인터페이스를 제공하며 최신 브라우저에 내장되어 있어 따로 설치할 필요가 없다.
반면 Axios는 third-party library로 Axios를 사용하려면 CDN이나 npm/yarn 등을 통해 설치해야 한다. Axios도 Fetch와 마찬가지로 브라우저나 node.js 환경 내에서 실행된다.
Fetch와 Axios는 둘 다 promise 기반의 HTTP client이다. 즉, HTTP 요청을 위해 Fetch나 Axios를 사용했다면 resolve나 reject된 promise가 리턴된다는 것이다.
그럼 이제부터 이 둘을 비교해보자.
Fetch는 2개의 인자를 받는다. 첫 번째 인자는 자원을 얻어오고자 하는 URL이며 두 번째 인자는 optional로 요청을 만드는 데에 필요한 설정이 들어있는 객체이다.
만약 설정 옵션 없이 첫 번째 인자만을 사용한다면 기본적으로 GET 요청을 보낸다.
fetch(url)
설정 옵션 인자를 포함해 두 가지의 인자를 모두 사용한다면 HTTP 요청을 아래와 같이 커스텀할 수 있다.
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
Axios의 문법은 비교적 간단하지만 다양하게 쓸 수 있다.
먼저, Fetch와 같이 url만을 인자로 넘겼다면 기본적으로 GET 요청을 보낸다.
axios(url)
같은 GET 요청을 다르게 쓰자면, 아래와 같이 axios의 HTTP 메서드를 사용할 수 있으며 설정 옵션을 넘기는 것도 가능하다.
axios.get(url)
axios.get(url, {
// config option
})
아예 fetch랑 같은 문법을 사용해 첫 번째 인자는 url로, 두 번째 인자는 설정 옵션 객체로 쓰는 것도 가능하다.
axios(url, {
method: 'get',
headers: {}
})
마지막으로 아예 모든 옵션을 객체로 만들어 인자로 넘기는 것도 가능하다.
axios({
method: 'get',
url: url,
headers: {}
})
이처럼 같은 GET 요청을 보내는 데에도 쓰는 문법은 다양하게 존재한다.
fetch(url)
.then(res => res.json())
.then(console.log)
fetch()
는 .then()
메서드를 사용해 응답 결과를 다뤄야 하는 promise를 리턴한다. 이때 응답은 Response 객체이고 직접 JSON 응답 본문을 받을 수 없다.
Response 객체에서도 우리가 필요한 것은 HTTP 응답 전체가 아닌 JSON 본문 콘텐츠이기 때문에 이를 추출하기 위해서는 .json()
메서드를 호출해야 한다.
따라서 JSON으로 파싱하기 위해 또 다른 promise가 리턴되며 결국 fetch를 사용하면 최소 두 개의 .then()
체인을 사용해야 한다.
axios.get(url)
.then(response => console.log(response.data));
반면 Axios는 응답 데이터에 대해 JSON 형식을 기본값으로 사용해 따로 .json()
함수를 호출하지 않아도 된다. 또한 응답 결과는 항상 응답 객체의 data 속성에서 찾아볼 수 있다.
json이 아닌 다른 응답 타입을 사용하고 싶다면 아래와 같이 responseType을 명시하면 된다.
axios.get(url, {
responseType: 'json' // options: 'arraybuffer', 'document', 'blob', 'text', 'stream'
})
data를 전송하기 위해서는 보낼 data를 JSON 문자열로 만드는 과정이 필요하다.
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
응답 결과를 JSON으로 파싱하기 위해 .json()
을 사용했듯이 request body에 데이터를 할당하기 위해 JSON.stringify()
가 필요하다. 이때 Content-Type을 application/json으로 명시해줘야 한다.
axios.post(url, {
headers: {
'Content-Type': 'application/json',
},
data: data
})
Axios는 또 알아서 해준다. 그냥 설정 옵션 객체의 data 속성에 보내고 싶은 데이터를 넣어주면 된다. 예제에서는 Content-Type을 application/json으로 명시해줬지만 이것도 사실 기본값으로 들어간다.
Fetch나 Axios 둘 다 resolved 혹은 rejected인 promise를 리턴한다. 만약 promise가 rejected 상태라면 .catch()
를 통해 에러를 다룰 수 있다.
Fetch에서의 에러 핸들링은 조금 까다로운 편이다.
fetch()가 반환하는 promise 객체는 200번대가 아닌 status code를 받아도 reject되지 않는다. 200이든 404든 다를 것 없이 fetch()의 입장에서는 요청을 보냈고 응답을 받은 것이다. 다만 status code가 200번대가 아닐 경우 ok
속성이 false로 설정된다.
Fetch의 promise가 reject되는 경우는 네트워크 연결이 실패하거나 해서 아예 요청을 보내지 못했을 경우이다.
결국 에러를 다루기 위해서는 .then()
안에서 관련 처리를 별도로 해줘야 한다.
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(
`This is an HTTP error: The status is ${response.status}`
);
}
return response.json();
})
.then(console.log)
.catch(err => {
console.log(err.message);
});
이처럼 throw new Error로 에러를 던지고 이를 catch에서 받아 에러 핸들링을 할 수 있다.
axios.get(url)
.then((response) => console.log(response.data))
.catch((err) => {
console.log(err.message);
});
Axios의 promise는 status code가 200번대가 아니면 반드시 reject된다. 그러니 Fetch와 다르게 별도의 에러 처리 과정이 필요없이 바로 .catch()
구문에서 처리할 수 있다.
만약 에러에 대해 좀 더 자세한 정보가 필요하다면 err 객체의 response나 request 속성에서 관련 정보를 얻을 수 있다.
.catch((err) => {
// handling error
if (err.response) {
// Request made and server responded
const { status, config } = err.response;
if (status === 404) {
console.log(`${config.url} not found`);
}
if (status === 500) {
console.log("Server error");
}
} else if (err.request) {
// Request made but no response from server
console.log("Error", err.message);
} else {
// some other errors
console.log("Error", err.message);
}
});
err 객체의 response
속성은 클라이언트가 상태 코드가 200번대가 아닌 응답을 받았음을 나타낸다. 그리고 err 객체의 request
속성은 요청이 만들어졌으나 클라이언트가 응답을 받지 못했음을 나타낸다. 만약 둘 다 없을 경우에는 요청을 설정하는 동안 에러가 발생한 경우에 해당한다.
Fetch에서의 응답 타임아웃/요청 취소는 AbortController
인터페이스를 통해 이루어진다.
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 4000);
fetch(url, {
signal: signal
})
.then((response) => response.json())
.then(console.log)
.catch((err) => {
console.error(err.message);
});
controller 객체를 만들고, 이를 통해 signal
과 .abort()
메서드에 접근한다. fetch의 설정 옵션에서 signal 속성에 controller 객체의 signal을 넘기고 setTimeout에서 설정한 시간만큼 기다렸다가 .abort()
메서드가 호출되는 순간까지 응답이 돌아오지 않으면 바로 요청을 종료시킨다.
물론 이것도 Axios에서는 간편해진다.
axios.get(url, {
timeout: 4000, // default is `0` (no timeout)
})
.then((response) => console.log(response.data))
.catch((err) => {
console.log(err.message);
});
AbortController 인터페이스, setTimeout 등을 사용할 필요 없이 그저 timeout
속성에 원하는 ms값을 넣어주면 된다.
링크를 통해 axios와 fetch를 비교해보면 위와 같은 성능 비교 결과를 얻을 수 있다.
fetch가 axios보다 살짝 빠르다.
이렇게 비교를 해보니 왜 아직 Axios가 건재한지 알 것 같았다.
결국 Fetch냐 Axios냐를 선택하는 것은 의존성을 줄일 것이냐 아님 의존성을 추가하는 대신 편리함을 얻을 것이냐의 문제인 것 같다.
어차피 Axios가 하는 것들은 모두 Fetch로 구현할 수 있으니 의존성을 늘리는 대신 조금만 더 신경쓰면서 살자! 라고 생각하고 있었는데 생각보다 많은 것들을 신경쓰고 있었던 것 같다.
하지만 우리 Fetch 많이 사랑해 주세요.