이 글의 코드는 전부 JavaScript다. 이름은 자바와 닮았지만 다른 언어이고, 브라우저 안에서 돌아간다. 자바만 아는 사람이 처음 보면 낯선 문법이 몇 가지 있어서, 코드마다 뒤에서 한 줄씩 따라간다.
프론트엔드 — 사용자가 브라우저에서 보는 화면 쪽 프로그램 — 는 서버에 데이터를 달라고 하거나 보낼 일이 계속 생긴다. 이 「서버의 API를 부르는 일」을 이 글에서는 API 호출이라고 부른다. 지금은 당연하게 쓰는 기술이지만, 이것이 어떻게 진화해 왔는지, 그리고 왜 지금의 형태로 자리 잡았는지는 따로 생각해 볼 만하다.
30년이 넘는 웹 개발 역사 속에서 API 호출 방식은 단순한 기술 변화를 넘어, 사용자가 화면을 쓰며 겪는 경험(UX)과 개발자가 코드를 쓰며 겪는 경험(DX)의 변화를 반영한다. 사용자는 화면이 덜 깜빡이길 바랐고, 개발자는 같은 일을 더 짧고 덜 틀리게 쓰고 싶어 했다. 아래의 단계마다 둘 중 하나, 또는 둘 다가 움직인다.
여기서는 Form Submit의 시대부터 지금의 Fetch API와 Axios까지, 프론트엔드 API 호출의 변천사를 시간 순서로 따라간다.
1990년대에는 데이터를 보낼 때마다 페이지 전체를 새로 받았다
1995년 - Form Submit은 입력값을 보내고 새 페이지를 통째로 받는다
<form method="POST" action="/api/submit">
<input type="text" name="username" />
<button type="submit">전송</button>
</form>
이 코드는 HTML — 웹 페이지의 뼈대를 꺾쇠 태그로 적는 언어 — 이다. 한 줄씩 보면 이렇다.
<form method="POST" action="/api/submit">: 입력 칸 묶음(폼)을 시작한다. 전송하면/api/submit주소로, HTTP의 POST 방식(「데이터를 보낸다」)으로 요청을 보내라는 뜻이다<input type="text" name="username" />: 글자를 입력하는 칸이다. 보낼 때 이 칸의 값이username이라는 이름표를 달고 간다<button type="submit">전송</button>: 누르면 폼을 전송하는 버튼이다
초기 웹에서 브라우저가 서버로 데이터를 보내는 방법은 이 HTML Form Submit이었다.
문제는 전송한 다음에 일어나는 일이다. 서버 — 요청을 받아 처리하고 응답을 돌려주는 쪽 — 는 데이터를 처리한 뒤 응답으로 새 HTML 페이지를 통째로 돌려주고, 브라우저는 그 새 페이지를 처음부터 다시 그린다. 지금 보던 화면을 버리고 새 화면으로 갈아타는 것이라 다음 일이 따라온다.
- 요청할 때마다 전체 페이지가 새로고침된다
- 지금 화면에 입력해 두었던 다른 칸의 값도 함께 사라진다. 새로 받은 페이지에는 그 값이 들어 있지 않기 때문이다
- 사용자로서는 버튼 하나 누를 때마다 화면이 하얗게 깜빡이는 나쁜 경험이 된다
하지만 다른 방법이 없었기에, 당시 웹 개발자는 이 방식을 받아들여야 했다.
1996년 - Image Pixel Tracking은 보이지 않는 그림을 불러 데이터를 몰래 보낸다
<img src="/track?event=pageView&userId=123"
width="1" height="1"
style="display:none;" />
<img src="...">: 이미지를 띄우는 태그다. 브라우저는 그림을 받아 오려고src에 적힌 주소로 서버에 요청을 보낸다/track?event=pageView&userId=123: 주소 뒤?부터는 쿼리 파라미터다.event=pageView와userId=123두 값을&로 이어 붙였다width="1" height="1": 가로세로 1픽셀짜리로 띄운다style="display:none;": 아예 화면에 보이지 않게 숨긴다
개발자들은 이 점을 이용했다. 눈에 보이지 않는 1×1 픽셀 이미지 태그를 페이지에 넣으면, 브라우저가 그림을 받으려고 src 주소로 요청을 보낸다. 서버는 그 주소에 붙은 쿼리 파라미터를 읽어 「123번 사용자가 이 페이지를 봤다」를 기록한다. 그림을 부르는 척하면서 데이터를 보내는 것이다.
이 방식의 성질은 이렇다.
- 페이지가 새로고침되지 않는다. 이미지 하나를 받는 일일 뿐이라서다
- 하지만 서버가 돌려줄 수 있는 것은 그림 한 장이다. 서버가 「저장했다」나 「이런 데이터가 있다」 같은 내용을 페이지에 알려 줄 길이 없어서, 보내기만 하는 한쪽 방향 통신이다
- 그래서 서버에서 데이터를 받아 와 화면에 쓰는 데에는 쓸 수 없다
2000년대에는 페이지를 바꾸지 않고 데이터만 주고받는 Ajax가 시작됐다
이 시기가 웹 개발의 진짜 변곡점이었다. 페이지 전체를 다시 받지 않고, 자바스크립트로 필요한 데이터만 서버에 요청해서 화면 일부만 바꾸는 방식이 가능해졌다. 이 방식을 Ajax라고 부른다.
1999-2006년 - XMLHttpRequest로 페이지를 그대로 둔 채 서버에 요청할 수 있게 됐다
XMLHttpRequest(줄여서 XHR)는 브라우저에 들어 있는 객체로, 페이지를 새로 불러오지 않고 서버에 요청을 보내고 응답을 받게 해 준다. 이름에 XML이 들어 있지만 XML 말고 다른 형식의 데이터도 주고받을 수 있다.
// 당시 코드의 예시
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
var xhr = new XMLHttpRequest();: 요청 객체를 하나 만든다.var는 변수를 만드는 키워드다. 자바와 달리 타입을 적지 않는다xhr.open('GET', '/api/data', true);:/api/data에 GET 방식(「데이터를 달라」)으로 보낼 준비를 한다. 세 번째 인자true는 「응답을 기다리며 멈춰 있지 말고, 응답이 오면 알려 달라」(비동기)는 뜻이다. 웹 기술의 참고 문서인 MDN(Mozilla가 운영하는 MDN Web Docs, 이 글에서 「MDN 문서」라고 부른다)에 따르면 기본값도true다xhr.onload = function() { ... };: 응답이 다 도착하면 부를 함수를onload자리에 맡겨 둔다. 자바스크립트는 함수를 값처럼 변수나 속성에 넣을 수 있다. 이렇게 「나중에 불러 달라」고 맡겨 두는 함수를 콜백이라고 한다if (xhr.status === 200): 응답의 HTTP 상태 코드가 200(성공)인지 본다.===는 값과 타입이 둘 다 같은지 비교한다console.log(xhr.responseText);: 서버가 보낸 본문 글자를 브라우저 콘솔에 찍는다xhr.send();: 실제로 요청을 보낸다
실행 순서는 적힌 순서와 다르다. onload에 함수를 넣는 줄은 함수를 맡겨 두기만 하고 실행하지 않는다. send()로 요청이 나가고, 시간이 지나 응답이 도착한 뒤에야 브라우저가 맡겨 둔 함수를 불러 준다. 그 사이에 페이지는 그대로 있고 사용자는 화면을 계속 쓸 수 있다.
XMLHttpRequest가 바꾼 것은 이렇다.
- 화면 뒤에서 통신한다: Form Submit은 응답으로 페이지를 갈아탔지만, XHR은 페이지를 그대로 둔 채 요청과 응답이 화면 뒤에서 오간다
- 양방향으로 주고받는다: 이미지 픽셀과 달리 서버가 돌려준 데이터를 자바스크립트가 받아 쓸 수 있다
- 사용자 경험이 크게 바뀌었다: Gmail, Google Maps처럼 페이지 이동 없이 움직이는 웹 앱이 가능해졌다
하지만 실무에서 쓰기는 여전히 고통스러웠다. 아래는 데이터를 보내고, 성공 · 실패 · 네트워크 끊김을 각각 처리하는 코드다.
// XMLHttpRequest의 복잡한 에러 처리
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/submit', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
console.log(data);
} catch(e) {
console.error('JSON 파싱 에러');
}
} else if (xhr.status === 401) {
// 토큰 갱신 처리
} else {
console.error('요청 실패');
}
}
};
xhr.onerror = function() {
console.error('네트워크 에러');
};
xhr.send(JSON.stringify({ name: 'John' }));
xhr.setRequestHeader('Content-Type', 'application/json');: 요청에 헤더를 붙인다. Content-Type이application/json이면 「본문이 JSON 형식이다」라는 뜻이다xhr.onreadystatechange = function() { ... }: 요청의 진행 단계(readyState)가 바뀔 때마다 불리는 함수를 맡긴다if (xhr.readyState === 4):readyState는 0부터 4까지의 숫자다. MDN 문서에 따르면 4(DONE)가 「요청이 끝났다」이고, 그 앞 숫자들은 준비 중 · 헤더 받음 · 본문 받는 중 같은 중간 단계다. 그래서 4가 될 때까지는 아무것도 하지 않는다JSON.parse(xhr.responseText): 서버가 보낸 JSON 글자를 자바스크립트 객체로 바꾼다. 글자가 JSON 모양이 아니면 예외가 나서catch로 간다.console.error는console.log처럼 콘솔에 찍되, 에러 표시를 달아 찍는다xhr.status === 401: 401은 「누구인지 확인이 안 된다」는 실패 코드다. 로그인 증표(토큰)가 만료됐을 때 흔히 받는다. 그때 새 토큰을 받아 오는 처리를 여기에 넣는다else: 200도 401도 아닌 나머지 상태 코드다. 서버까지는 갔지만 서버가 실패라고 답한 경우다xhr.onerror: 서버의 답 자체를 못 받은 경우다. 인터넷이 끊겼거나 서버에 닿지 못하면 상태 코드도 없이 여기로 온다. 바로 위 「요청 실패」는 서버가 실패라고 답한 것이고, 이쪽은 답이 아예 오지 않은 것이다JSON.stringify({ name: 'John' }):{ name: 'John' }은 클래스 없이 중괄호로 바로 만든 객체다.stringify가 이것을{"name":"John"}이라는 JSON 글자로 바꿔 본문에 싣는다
XMLHttpRequest의 문제는 이렇다.
- 쓰는 방법이 직관적이지 않다: 「끝났나」를 알려면
readyState가 4인지 매번 직접 확인해야 한다 - 에러 처리가 복잡하다: 상태 코드마다, 네트워크 에러마다 처리 자리를 손으로 나눠야 한다
- 콜백 지옥: 위 코드는 요청이 하나라 콜백이 한 겹이다. 그런데 「A 요청이 끝나면 그 결과로 B 요청을 보내고, B가 끝나면 C를 보낸다」가 되면 콜백 안에 콜백이 들어가고, 그 안에 또 들어가 코드가 오른쪽으로 계속 밀린다
- 간단한 요청에도 보일러플레이트가 많다: 위의 열 줄 넘는 틀을 요청마다 거의 똑같이 다시 써야 한다
2006년 - jQuery Ajax가 XMLHttpRequest를 감싸 짧게 만들었다
jQuery는 화면 요소를 다루는 일을 짧은 코드로 하게 해 주던 자바스크립트 라이브러리다. 그 안에 XHR을 감싼 $.ajax 함수가 들어 있었다.
// jQuery가 감싼 방식 - 훨씬 간단했음
$.ajax({
url: '/api/data',
type: 'GET',
success: function(data) {
console.log(data);
},
error: function() {
console.error('요청 실패');
}
});
$: jQuery 라이브러리를 부르는 이름이다.$.ajax(...)는 jQuery가 가진ajax함수를 부른다{ url: ..., type: ..., success: ..., error: ... }: 설정을 객체 하나에 담아 넘긴다. 어디에(url), 어떤 방식으로(type) 보낼지 적는다success: function(data) { ... }: 성공했을 때 부를 콜백이다.data에 서버가 보낸 데이터가 들어온다error: function() { ... }: 실패했을 때 부를 콜백이다
XHR 코드와 견주면, readyState를 확인하는 일과 성공 · 실패를 가르는 일을 jQuery가 대신 해 준다. 개발자는 「성공하면 이것, 실패하면 저것」만 적는다.
jQuery Ajax가 나오면서 이렇게 됐다.
- XMLHttpRequest의 복잡함이 jQuery 안으로 숨겨졌다
- 널리 쓰이는 웹 개발 도구가 됐다
- 프론트엔드 개발자가 같은 일을 훨씬 적은 코드로 하게 됐다
2010년대까지 jQuery Ajax는 실무의 표준이었다.
2010년대에는 서버가 먼저 말을 거는 통신 방식이 따로 나왔다
웹이 복잡해지면서, 「브라우저가 묻고 서버가 답한다」는 요청-응답 방식만으로는 부족해졌다.
2011년 - WebSocket은 연결을 열어 두고 양쪽에서 메시지를 보낸다
// WebSocket으로 실시간 양방향 통신
const ws = new WebSocket('ws://server.com/stream');
ws.onmessage = function(event) {
console.log('서버로부터 메시지:', event.data);
};
ws.send('클라이언트 메시지');
const ws = new WebSocket('ws://server.com/stream');: 서버와 WebSocket 연결을 연다.const는 다시 대입하지 않을 변수를 만드는 키워드이고, 주소의ws://는 「WebSocket 연결」이라는 표시다ws.onmessage = function(event) { ... }: 서버가 메시지를 보낼 때마다 부를 콜백을 맡긴다. 브라우저가 메시지를 받으면 그 정보를event객체에 담아 이 함수를 불러 주고, 메시지 내용은event.data에 있다ws.send('클라이언트 메시지');: 같은 연결로 브라우저 쪽에서도 메시지를 보낸다. 클라이언트는 연결을 거는 쪽, 여기서는 브라우저다
왜 나왔나. 보통의 HTTP는 브라우저가 물어야 서버가 답한다. 서버에 새 소식이 생겨도 브라우저가 묻기 전에는 먼저 알려 줄 방법이 없다. 그래서 다음 같은 기능을 만들기 어려웠다.
- 실시간 알림, 라이브 채팅
- 주식 시세처럼 계속 바뀌는 데이터
WebSocket은 연결을 한 번 열어 두고, 그 연결로 서버도 원할 때 먼저 메시지를 보낼 수 있게 했다.
2012년 - Server-Sent Events는 서버에서 브라우저로만 소식을 보낸다
// 서버에서 브라우저로 한 방향으로만 메시지가 계속 흘러온다
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = function(event) {
console.log('서버로부터:', event.data);
};
new EventSource('/api/stream'): SSE(Server-Sent Events) 연결을 연다eventSource.onmessage = ...: 서버가 소식을 보낼 때마다 부를 콜백이다. WebSocket과 달리send가 없다. 브라우저는 받기만 한다
WebSocket처럼 양쪽이 다 말할 필요 없이, 서버가 브라우저 쪽으로 소식을 밀어 보내기만(푸시) 하면 되는 경우가 많았다. 알림 목록이나 시세판이 그렇다. 그럴 때는 이 단순한 쪽으로 충분했다.
2014년 - Navigator.sendBeacon은 페이지를 떠날 때도 데이터를 보낸다
navigator는 브라우저가 자바스크립트에 미리 넣어 주는 객체로, 브라우저 자신에 대한 기능을 모아 둔 곳이다. sendBeacon은 그 안의 함수로, 작은 데이터를 서버에 보내는 일만 한다.
// 페이지가 화면에서 사라질 때 분석 데이터를 보낸다
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/api/analytics', JSON.stringify({
sessionId: '123',
duration: 3600
}));
}
});
document.addEventListener('visibilitychange', function() { ... }): 페이지가 보이다 안 보이게 되거나 그 반대가 될 때 부를 함수를 건다(이벤트 리스너).document는 지금 페이지를 가리키는 객체다if (document.visibilityState === 'hidden'): 페이지가 안 보이게 된 경우 — 탭을 닫거나 다른 탭 · 앱으로 넘어간 경우 — 에만 보낸다navigator.sendBeacon('/api/analytics', ...): 이 페이지에 머문 시간 같은 분석 데이터를/api/analytics로 보낸다
왜 따로 필요했나. MDN 문서에 따르면, 페이지가 닫히려는 순간에 보낸 비동기 XHR 요청은 브라우저가 보내지 않고 버릴 수 있다. 그렇다고 요청이 끝날 때까지 페이지를 붙잡아 두면 다음 페이지가 늦게 뜬다. sendBeacon은 데이터를 브라우저에 맡겨 두면, 페이지가 사라져도 브라우저가 나중에 보내 준다. 사용자가 페이지를 떠나는 순간에도 데이터를 놓치지 않고 보내야 하는 분석 도구의 요구에서 나왔다.
페이지를 떠날 때 부르는 이벤트라면 beforeunload · unload · pagehide(셋 다 페이지를 떠나거나 닫히는 순간에 불리는 이벤트다)가 먼저 떠오른다. 이 글의 예제도 처음에는 beforeunload에 걸었다. 그런데 MDN은 이 셋이 특히 모바일에서 아예 불리지 않는 경우가 많아 믿을 수 없다며 visibilitychange를 쓰라고 권한다. 그래서 위 예제는 visibilitychange에 걸었다.
2015년에 Fetch API가 브라우저 기본 기능으로 들어왔다
Fetch API는 Promise로 XMLHttpRequest의 콜백을 대신하려 했다
fetch는 라이브러리를 설치하지 않아도 브라우저에 기본으로 들어 있는 요청 함수다. XHR처럼 콜백을 맡기는 대신 Promise를 돌려준다. Promise는 「나중에 끝날 작업의 결과」를 담는 객체로, .then(...)에 할 일을 담은 함수를 넘겨 두면 결과가 왔을 때 그 함수를 불러 준다.
// 모던한 Promise 기반 API
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('요청 실패:', error));
fetch('/api/data'):/api/data에 GET 요청을 보내고, 곧바로 Promise를 돌려준다.then(response => response.json()): 응답이 오면 본문을 JSON으로 읽는다.response => response.json()은 화살표 함수다. 「response를 받아response.json()을 돌려준다」는 짧은 함수다.response.json()도 본문을 다 읽는 데 시간이 걸려서 Promise를 돌려준다.then(data => console.log(data)): 앞 단계의 JSON 읽기가 끝나면 그 결과 객체(data)를 찍는다. 앞.then의 함수는 Promise를 돌려줬는데 여기에 Promise가 아니라 결과 객체가 들어오는 이유는 이렇다 — MDN 문서에 따르면.then안의 함수가 Promise를 돌려주면, 다음.then은 그 Promise가 끝날 때까지 기다렸다가 끝난 결과 값을 받는다.catch(error => ...): 앞 단계 중 어디서든 실패하면 여기로 온다
XHR 코드와 견주면, 「끝나면 이것, 그다음 이것」이 안으로 파고들지 않고 .then으로 아래로 이어진다. 요청이 세 개 이어져도 .then이 세 줄 늘어날 뿐 들여쓰기가 깊어지지 않는다.
Fetch API가 풀려던 문제는 이렇다.
- Promise를 쓴다: 위처럼 단계가 옆으로 파고들지 않고 아래로 이어져서 콜백 지옥을 벗어난다
- 쓰는 모양이 직관적이다:
fetch(주소)함수 하나로 「이 주소에 요청한다」가 드러난다 - 표준 기능이다: 라이브러리 없이 브라우저가 직접 지원한다
하지만 실무에서 써 보면 빈자리가 있었다.
// Fetch API의 숨겨진 문제 1: 에러 처리
fetch('/api/data')
.then(response => {
// 404, 500도 reject되지 않음!
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => console.error(error));
// Fetch API의 숨겨진 문제 2: 요청 취소 불가능
// (AbortController는 2017년 이후에 추가됨)
// Fetch API의 숨겨진 문제 3: 타임아웃 없음
// 무한 대기할 수 있음
// Fetch API의 숨겨진 문제 4: 인터셉터 기능 없음
// 모든 요청에 토큰을 붙여야 함
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
})
코드 안의 response => { ... return response.json(); }처럼 화살표 함수의 몸통이 여러 줄이면 중괄호로 감싸고, 돌려줄 값은 return으로 적는다. 앞의 response => response.json()처럼 한 줄이면 중괄호와 return을 생략한 것이다. 마지막 token은 로그인할 때 받아 저장해 둔 토큰 값이 들어 있는 변수라고 치고, 만드는 코드는 생략했다.
하나씩 보면 이렇다.
- 문제 1 — 404 · 500도 성공으로 온다. 404는 「그런 주소가 없다」, 500은 「서버 안에서 에러가 났다」는 상태 코드다. MDN 문서에 따르면 fetch의 Promise는 네트워크가 끊기는 것처럼 요청 자체가 실패할 때만 실패(reject)하고, 서버가 404나 500으로 답해도 성공으로 끝난다. 그래서
.catch만 믿으면 서버가 실패라고 답한 것을 놓친다.response.ok(상태 코드가 200번대면true)를 직접 확인하고, 아니면throw로 예외를 던져.catch로 보내야 한다.`HTTP error! status: ${response.status}`는 백틱으로 감싼 템플릿 리터럴로,${}자리에 상태 코드 값이 들어간다 - 문제 2 — 요청을 취소할 수 없었다. 처음 나온 fetch에는 보낸 요청을 중간에 멈추는 방법이 없었다. 나중에 AbortController가 추가되면서 가능해졌다
- 문제 3 — 타임아웃이 없다. fetch에는 「몇 초까지만 기다린다」는 설정이 없어서, 서버가 답을 안 주면 하염없이 기다릴 수 있다
- 문제 4 — 인터셉터가 없다. 로그인한 사용자의 요청에는
Authorization: Bearer 토큰값헤더(Bearer 토큰)를 붙여 「나는 로그인한 사람이다」를 증명한다. fetch에는 「모든 요청에 이걸 자동으로 붙여라」를 한곳에 적어 둘 인터셉터가 없어서, 위 코드처럼 요청마다 헤더를 손으로 붙여야 한다
실무에서 만난 Fetch API의 부족함:
| 기능 | Fetch API | 필요성 |
|---|---|---|
| 요청 취소 | 없음 (나중에 AbortController 추가) | 높음 - 중복 요청 방지 |
| 타임아웃 | 없음 | 높음 - 무한 대기 방지 |
| 인터셉터 | 없음 | 높음 - 글로벌 에러 처리 |
| 에러 처리 | 반직관적 | 높음 - 명확한 실패 판단 |
| 요청 재시도 | 수동 구현 필요 | 중간 |
표의 「필요성」 칸을 풀면 이렇다.
- 중복 요청 방지: 사용자가 검색어를 바꿔 다시 검색하면 앞 요청의 결과는 더 필요 없다. 앞 요청을 취소하지 못하면 늦게 도착한 옛 결과가 새 결과를 덮어쓸 수 있다
- 글로벌 에러 처리: 「어느 요청이든 401이 오면 로그인 화면으로 보낸다」처럼 모든 요청에 똑같이 적용할 처리를 한곳에 두는 것이다
- 요청 재시도: 서버가 잠깐 바빠서 실패했을 때 같은 요청을 한 번 더 보내는 것이다
Axios는 Fetch의 빈자리를 기능으로 채워 실무 표준이 됐다
Axios는 따로 설치해서 쓰는 HTTP 요청 라이브러리다. 자바스크립트 라이브러리를 올려 두는 공개 저장소인 npm의 기록을 보면 첫 버전(0.1.0)은 2014년 8월에 올라왔다. 그러니 Fetch보다 먼저 나왔다. 이 글에서 Fetch 다음에 놓은 것은, 앞 절에서 본 Fetch의 빈자리를 Axios가 처음부터 갖추고 있어서다. Axios 공식 문서에 따르면 브라우저에서는 기본적으로 XMLHttpRequest를 감싸서 동작한다.
// Axios의 우아함
axios.get('/api/data')
.then(response => console.log(response.data))
.catch(error => console.error(error.message));
// 요청 취소
const source = axios.CancelToken.source();
axios.get('/api/data', { cancelToken: source.token });
source.cancel('요청 취소됨');
// 타임아웃
axios.get('/api/data', { timeout: 5000 });
// 인터셉터 - Fetch API에서는 이걸 직접 구현해야 함
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${token}`;
return config;
}
);
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 토큰 갱신 처리
}
return Promise.reject(error);
}
);
axios.get('/api/data'): GET 요청을 보내고 Promise를 돌려준다. fetch와 달리 본문을 JSON으로 읽는 단계(response.json())가 따로 없다. Axios가 미리 읽어서response.data에 넣어 준다axios.CancelToken.source(): 취소용 짝을 하나 만든다.source.token은 요청에 넘기는 표,source.cancel()은 그 표를 단 요청을 멈추는 스위치다. 요청을 보낼 때{ cancelToken: source.token }으로 표를 달아 두고,source.cancel(...)을 부르면 그 요청이 취소된다. 예시라서 보낸 바로 다음 줄에서 취소했지만, 실제로는 사용자가 검색어를 바꿨을 때처럼 앞 요청이 필요 없어진 순간에 부른다. 다만 Axios 문서는 지금 이CancelToken을 더 쓰지 말라(deprecated)고 하고, v0.22.0부터 지원하는 AbortController를 권한다{ timeout: 5000 }: 5000밀리초(5초) 안에 응답이 없으면 실패로 처리한다axios.interceptors.request.use(config => { ... }): 모든 요청이 나가기 직전에 부를 함수를 등록한다.config는 나갈 요청의 설정(주소 · 헤더 등)이고, 여기에 토큰 헤더를 붙여 돌려주면 그 설정대로 요청이 나간다. 이 한 번의 등록으로 모든 요청에 토큰이 붙는다axios.interceptors.response.use(성공 함수, 실패 함수): 모든 응답이 돌아온 직후에 부를 함수 둘을 등록한다. 첫 번째response => response는 성공한 응답을 그대로 넘긴다. 두 번째는 실패했을 때 불린다error.response?.status === 401:?.는 옵셔널 체이닝이다. 네트워크 에러라error.response가 없으면 에러를 내지 않고 넘어간다. 401이면 토큰을 새로 받는 처리를 여기 한곳에 둔다return Promise.reject(error);: 처리한 뒤에도 그 요청은 실패로 남겨, 요청을 보낸 쪽의.catch로 가게 한다
Axios가 Fetch API 대신 쓰이게 된 이유는 이렇다.
- 필요한 게 처음부터 다 들어 있다: 「배터리 포함」이라는 말을 쓴다. 장난감을 사면 건전지를 따로 사지 않아도 바로 켤 수 있다는 비유다. 취소 · 타임아웃 · 인터셉터를 따로 만들지 않아도 된다
- 에러 처리가 일관된다: Axios 문서에 따르면 상태 코드가 200~299 밖이면 Promise를 실패로 돌린다. fetch의 「문제 1」과 달리 404 · 500도
.catch로 오니response.ok를 직접 확인할 필요가 없다 - 인터셉터: 모든 요청에 똑같이 할 일을 한곳에 둔다
- 요청 취소: 위의 CancelToken으로 취소할 수 있었다
- 개발자 경험: 같은 일을 더 짧은 코드로 한다
변화의 흐름을 한눈에 보면 단계마다 앞 단계의 불편이 다음을 불렀다
Form Submit
페이지 새로고침"] -->|"문제 해결"| B["1996
Image Pixel
보내기만 가능"] B -->|"혁명"| C["1999
XMLHttpRequest
Ajax 시작"] C -->|"복잡함"| D["2006
jQuery Ajax
대중화"] D -->|"필요성 증가"| E["2011-2014
WebSocket/SSE
실시간"] D -->|"표준화"| F["2015
Fetch API
Promise 기반"] F -->|"부족함"| G["Axios
(2014 첫 버전)
실무 표준"] style A fill:#e8f4f8,stroke:#4a7c7e,color:#000 style B fill:#e8f4f8,stroke:#4a7c7e,color:#000 style C fill:#d4e6f1,stroke:#2c5282,color:#000 style D fill:#d4e6f1,stroke:#2c5282,color:#000 style E fill:#f0e6d2,stroke:#6b5344,color:#000 style F fill:#d4e6f1,stroke:#2c5282,color:#000 style G fill:#d4e6f1,stroke:#2c5282,color:#000
화살표 위의 말이 다음 단계로 넘어간 이유다. jQuery Ajax 뒤에서 길이 둘로 갈린다. 하나는 서버가 먼저 말을 거는 실시간 통신(WebSocket/SSE)이고, 다른 하나는 XHR과 jQuery의 콜백 방식을 브라우저 표준으로 바꾼 Fetch다. Axios만 연도 순서가 아니라 「Fetch의 부족함을 채운 쪽」이라는 자리로 놓았다.
2020년대에는 Fetch와 Axios가 나란히 쓰인다
지금 프론트엔드 개발 현장에서는 세 갈래가 함께 쓰인다.
// 진영 1: "Fetch API로 충분하다" - 신규 프로젝트
fetch('/api/data')
.then(r => r.json())
.catch(e => console.error(e));
// 진영 2: "Axios가 실무의 현실" - 기존 프로젝트
axios.get('/api/data')
.catch(e => console.error(e));
// 진영 3: "Fetch + 커스텀 래퍼" - 절충
const api = {
async get(url, options = {}) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), options.timeout || 5000);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
if (!response.ok) throw new Error(response.statusText);
return response.json();
} finally {
clearTimeout(id);
}
}
};
진영 1과 2는 앞에서 본 코드를 줄인 것이다. 진영 3은 fetch를 직접 감싼 내 함수(래퍼)를 만들어, Axios가 주던 기능 중 필요한 것만 채운다.
const api = { async get(url, options = {}) { ... } };:get이라는 함수 하나를 가진 객체를 만든다. 앞으로api.get('/api/data')처럼 부른다.options = {}는 기본 매개변수로, 인자를 안 넘기면 빈 객체가 들어간다async와await:async를 붙인 함수 안에서는await로 Promise의 결과가 올 때까지 기다렸다가 다음 줄로 간다..then을 이어 붙이지 않고 위에서 아래로 평범하게 읽히는 코드가 된다. MDN 문서에 따르면 이때 멈추는 것은 이 함수의 남은 줄뿐이고, 브라우저의 다른 코드와 화면은 계속 돈다. 그래서 XHR의 비동기처럼 화면이 굳지 않는다new AbortController()와setTimeout(() => controller.abort(), ...): fetch에는 타임아웃 설정이 없어서(문제 3) 직접 만든다.setTimeout(함수, 시간)은 정해 둔 밀리초가 지나면 함수를 한 번 부르는 브라우저 기능이고,() => controller.abort()는 괄호가 비어 있는 화살표 함수 — 매개변수 없이controller.abort()만 하는 함수 — 다. 그래서 시간이 지나면abort()로 요청이 취소된다.setTimeout이 돌려주는id는 이 타이머의 번호로, 나중에 타이머를 치울 때 쓴다.options.timeout || 5000은options.timeout이 비어 있으면 5000(5초)을 쓰라는 뜻이다fetch(url, { ...options, signal: controller.signal }):...options는 스프레드 문법으로, 넘겨받은 설정을 그대로 펼쳐 넣는다. 거기에 취소 신호(signal)를 더해 보낸다if (!response.ok) throw ...: 404 · 500을 실패로 바꾼다(문제 1).response.statusText는 상태 코드에 붙는 짧은 설명 글자(404라면Not Found같은 것)다finally { clearTimeout(id); }: 응답이 먼저 왔든 실패했든, 걸어 둔 타이머를 치운다
왜 이렇게 나란히 쓰이게 됐나.
- Fetch API가 빈자리를 일부 채웠다: AbortController가 들어오면서 취소와 타임아웃을 fetch만으로 만들 수 있게 됐다. 다만 404 · 500을 성공으로 돌려주는 점은 그대로라서
response.ok는 여전히 직접 확인한다 - TypeScript와 번들러가 발전하면서 라이브러리를 더 신중하게 고르게 됐다: 번들러는 내 코드와 내가 쓰는 라이브러리를 한 번들로 묶는다. Axios를 쓰면 그만큼 사용자가 내려받는 파일이 커지고, 브라우저에 이미 있는 fetch는 그 비용이 없다. TypeScript가 이 선택에 어떻게 이어졌는지는 이 글에서 따로 확인하지 않았다
- 프로젝트 규모에 따라 고르는 것이 달라졌다:
- 간단한 프로젝트: Fetch API만으로 충분하다
- 복잡한 엔터프라이즈(회사의 큰 업무 시스템처럼 화면과 요청이 아주 많은 프로젝트): Axios가 주는 개발자 경험이 중요하다
- 라이브러리 대신 브라우저 표준 위에 필요한 것만 얹고 싶은 팀: Fetch에 직접 만든 래퍼를 씌운다
실무에서는 요청의 복잡도와 번들 크기로 Fetch와 Axios를 고른다
1. Fetch API는 요청이 단순하고 번들 크기가 중요할 때 고른다
이런 경우에 맞는다.
- 요청이 단순한 프로젝트
- 사용자가 받는 파일(번들) 크기를 줄여야 하는 경우. 라이브러리를 더 깔지 않으니 번들이 그대로다
- 최신 브라우저만 지원하는 경우. fetch는 2015년에 나와서 그 전 브라우저에는 없다
- 모든 요청에 공통으로 끼어드는 복잡한 처리가 필요 없는 경우
타임아웃이 필요하면 아래처럼 직접 만든다.
const fetchWithTimeout = (url, timeout = 5000) => {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(id));
};
const fetchWithTimeout = (url, timeout = 5000) => { ... };: 화살표 함수를 변수에 담았다. 자바스크립트는 함수도 값이라 변수에 넣을 수 있고, 이제fetchWithTimeout('/api/data')로 부른다const controller = new AbortController();: 요청을 취소할 수 있는 리모컨을 만든다const id = setTimeout(() => controller.abort(), timeout);:timeout밀리초 뒤에abort()를 부르도록 타이머를 건다.id는 나중에 이 타이머를 치울 때 쓰는 번호다fetch(url, { signal: controller.signal }): 요청에 리모컨의 신호를 연결해 보낸다. 시간이 다 돼abort()가 불리면 이 요청이 멈추고, fetch의 Promise는AbortError로 실패한다.finally(() => clearTimeout(id)): 응답이 시간 안에 왔든 실패했든, 끝나면 타이머를 치운다. 안 치우면 이미 끝난 요청에 대고 나중에abort()가 불린다
이렇게 「시간 안에 안 오면 취소」를 조합하면 그것이 곧 타임아웃이 된다.
2. Axios는 모든 요청에 공통 처리가 필요하고 프로젝트가 클 때 고른다
이런 경우에 맞는다.
- 엔터프라이즈급으로 복잡한 프로젝트(화면과 요청이 아주 많은 큰 업무 시스템)
- 모든 요청에 끼어드는 인터셉터가 꼭 필요한 경우
- 이미 Axios로 짜인 기존 프로젝트를 유지보수하는 경우
- 요청 재시도 · 취소 같은 기능이 필요한 경우
axios.defaults.timeout = 5000;
// 자동 재시도
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status >= 500) {
return retryRequest(error.config);
}
return Promise.reject(error);
}
);
axios.defaults.timeout = 5000;:defaults는 모든 요청에 적용되는 기본 설정이다. 요청마다 적지 않아도 전부 5초 타임아웃이 걸린다axios.interceptors.response.use(...): 모든 응답에 끼어드는 함수를 등록한다error.response?.status >= 500: 500번대는 서버 쪽 잘못이다. 서버가 잠깐 바빴을 수 있으니 다시 보내 볼 만하다return retryRequest(error.config);: 실패한 요청의 설정(error.config)으로 같은 요청을 다시 보낸다.retryRequest는 Axios에 들어 있는 함수가 아니라, 몇 번까지 다시 보낼지 등을 정해 직접 만들어야 하는 함수다. 이 글에는 그 몸통을 적지 않았다return Promise.reject(error);: 500번대가 아니면 다시 보내지 않고 실패로 넘긴다
변천사는 단계마다 웹 개발이 부딪힌 현실의 문제를 보여 준다
프론트엔드 API 호출의 30년 역사는 단순한 기술 교체가 아니다.
각 단계는 웹 개발이 마주한 현실의 문제를 보여 준다.
- 1995: 「페이지가 자꾸 새로고침된다」 → Form Submit의 한계
- 1999: 「화면 뒤에서 통신하고 싶다」 → XMLHttpRequest의 탄생
- 2006: 「코드가 너무 복잡하다」 → jQuery Ajax의 추상화
- 2011-2014: 「서버가 먼저 말을 걸어야 한다」 → WebSocket, SSE, sendBeacon
- 2015: 「콜백과 라이브러리 없이 표준으로 요청하고 싶다」 → Fetch API
- Axios: 「실무에서 필요한 것이 빠졌다」 → Axios의 확산
앞으로의 방향은 이렇게 본다.
- Fetch API는 계속 진화할 것이다. 취소는 AbortController로 이미 들어왔고, 인터셉터 같은 기능이 표준에 들어올지는 아직 모른다
- Axios는 안정적인 선택지로 남을 것이다
- 결국 개발자의 판단이다: 프로젝트의 규모와 복잡도에 맞는 선택이 있다
지금 쓰는 프로젝트에서 Fetch와 Axios 중 무엇을 왜 골랐는지 묻고 싶다
지금 진행 중인 프로젝트에서는 Fetch API와 Axios 중 어느 것을 쓰고 있는지, 그 선택의 이유는 무엇인지 궁금하다.
댓글로 나눠 주면 좋겠다. 실무의 다양한 경험이 모이면 더 나은 선택의 기준을 만들 수 있다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기