Fetch API와 AJAX
로컬 Mock API에 XHR과 fetch 요청을 보내고 HTTP 메서드·JSON·오류·취소·CORS를 비동기로 처리합니다.
우리는 8장에서 최신 ECMAScript 문법을 통해 더 견고하고 간결한 자바스크립트 코드를 작성하는 방법을 배웠습니다.
이제 다음 단계는 언어 자체를 넘어, 브라우저가 제공하는 웹 API(Web APIs)를 실전적으로 쓰는 것입니다.
DOM, 이벤트, 스토리지, 네트워크 통신 같은 기능은 모두 웹 API 위에서 동작합니다.
9장 웹 API와 브라우저 기능의 시작점은 서버 통신의 핵심인 AJAX와 Fetch API입니다.
사용자 경험을 부드럽게 만드는 가장 중요한 기술 중 하나입니다.
웹 페이지는 처음 로드될 때 서버에서 HTML, CSS, JavaScript를 받아옵니다.
하지만 이후 상호작용에서는 페이지 전체를 다시 받을 필요가 없는 경우가 많습니다.
필요한 데이터만 비동기로 주고받는 방식이 바로 AJAX입니다.
AJAX는 이름에 XML이 들어가지만, 실무에서는 JSON 같은 형식을 더 자주 사용합니다.
핵심은 전체 새로고침 없이 필요한 데이터만 교환한다는 점입니다.
과거에는 XMLHttpRequest가 중심이었고,
현재는 Promise 기반의 Fetch API가 기본 선택지가 되었습니다.
Fetch API는 브라우저에서 HTTP 요청을 만들고 응답을 Promise 흐름으로 처리하는 기준입니다. 메서드, 헤더, 본문, 오류 처리를 함께 봐야 합니다.
- XMLHttpRequest (XHR)
XMLHttpRequest 객체는 AJAX의 전통적인 구현체입니다.
- Fetch API: AJAX의 현대적인 표준
Fetch API는 Promise를 기반으로 비동기 네트워크 요청을 수행하는 현대적인 인터페이스입니다.
- CORS (Cross-Origin Resource Sharing)
Fetch API를 사용해 다른 도메인의 리소스에 접근할 때는 CORS (Cross-Origin Resource Sharing) 정책을 이해해야 합니다.
실습 API 준비 (로컬 Mock 서버)
외부 공개 API 상태에 영향받지 않도록, 이 절의 실습 URL은 http://localhost:4000 기준으로 맞춥니다.
포트 정책은 4000=Mock API, 4100=별도 백엔드/실시간 서버로 분리해 두면 트랙 병행 학습 시 충돌을 줄일 수 있습니다.
npx json-server --watch db.json --port 4000db.json에는 최소한 posts, users 배열을 준비해 두면 예제를 그대로 재현할 수 있습니다.
XMLHttpRequest (XHR)
XMLHttpRequest 객체는 AJAX의 전통적인 구현체입니다.
복잡하고 콜백 지옥에 빠지기 쉬웠지만, 웹 개발의 중요한 한 획을 그었습니다.
// // fetch API가 없을 때의 XMLHttpRequest 사용 예시 (간략)
// function fetchDataXHR() {
// const xhr = new XMLHttpRequest();
// xhr.open('GET', 'http://localhost:4000/posts/1'); // 요청 초기화
// xhr.onload = function() { // 응답이 로드되었을 때 (성공)
// if (xhr.status >= 200 && xhr.status < 300) {
// console.log("XHR 응답 성공:", JSON.parse(xhr.responseText));
// } else {
// console.error("XHR 응답 실패:", xhr.status);
// }
// };
// xhr.onerror = function() { // 요청 실패 (네트워크 오류 등)
// console.error("XHR 네트워크 오류 발생");
// };
// xhr.send(); // 요청 전송
// }
// fetchDataXHR();XMLHttpRequest는 상태 변화에 따른 이벤트 리스너를 등록하고, 성공/실패 여부를 직접 판단해야 하는 등 다소 번거로운 점이 있었습니다.
Fetch API: AJAX의 현대적인 표준
Fetch API는 Promise를 기반으로 비동기 네트워크 요청을 수행하는 현대적인 인터페이스입니다.
XMLHttpRequest보다 사용하기 쉽고, 더 강력하며, 더 많은 기능을 제공합니다.
fetch() 함수의 기본 사용법
fetch() 함수는 첫 번째 인자로 요청할 URL을 받으며, Promise를 반환합니다.
이 Promise는 네트워크 요청에 대한 Response 객체로 이행(resolve)됩니다.
Response 객체에는 HTTP 응답에 대한 정보(상태 코드, 헤더 등)가 담겨 있습니다.
실제 응답 본문(body)의 데이터(JSON, 텍스트 등)를 얻으려면 Response 객체의 메서드를 추가로 호출해야 합니다.
// GET 요청 예시
fetch('http://localhost:4000/posts/1') // Promise 반환
.then(response => {
// 응답 상태 확인: response.ok는 200-299 상태 코드를 나타냄
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
// 응답 본문을 JSON으로 파싱 (Promise 반환)
return response.json();
})
.then(data => {
console.log("Fetch GET 응답 데이터:", data);
})
.catch(error => {
console.error("Fetch 요청 실패:", error);
});주의: fetch()는 네트워크 오류와 같이 실제 요청이 실패했을 때만 Promise를 거부(reject)합니다.
HTTP 상태 코드 404(Not Found)나 500(Server Error)과 같은 서버 응답 오류는 Promise를 거부하지 않고, Response 객체의 ok 프로퍼티가 false로 설정됩니다.
따라서 항상 response.ok를 확인하여 HTTP 오류를 처리해야 합니다.
다양한 HTTP 메서드와 옵션
fetch() 함수의 두 번째 인자로 init 객체를 전달하여 다양한 요청 옵션을 설정할 수 있습니다.
// POST 요청 예시
const newPost = {
title: '새로운 게시물',
body: '이것은 Fetch API로 작성된 새로운 게시물 내용입니다.',
userId: 1,
};
fetch('http://localhost:4000/posts', {
method: 'POST', // HTTP 메서드 지정
headers: {
'Content-Type': 'application/json; charset=UTF-8', // 요청 헤더 지정
},
body: JSON.stringify(newPost), // 전송할 데이터 (JSON.stringify로 문자열화)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log("Fetch POST 응답 데이터 (생성된 게시물):", data);
})
.catch(error => {
console.error("Fetch POST 요청 실패:", error);
});주요 init 객체 옵션
method:GET,POST,PUT,DELETE등 HTTP 메서드 (GET이 기본값).headers: 요청 헤더를 설정하는 객체 (Content-Type,Authorization등).body:POST,PUT등과 함께 전송할 데이터. 문자열(JSON.stringify),FormData객체 등이 될 수 있습니다.mode:cors(기본값),no-cors,same-origin. CORS 정책과 관련됩니다.cache:default,no-store,reload,force-cache등 캐시 정책.credentials:omit,same-origin,include. 인증 정보(쿠키, HTTP 인증) 전송 여부.referrer: 요청의 Referer 헤더.signal:AbortController와 함께 요청을 취소할 때 사용.
Fetch 옵션은 단순한 부가 설정이 아니라 요청의 목적, 데이터 형식, 인증 범위, 취소 조건을 서버와 맞추는 설계표입니다.
method, headers, body, credentials, signal은 서버 통신의 약속을 코드로 드러냅니다.
- 1method
GET은 조회, POST/PUT/PATCH/DELETE는 변경 요청에 씁니다.
- 2body
JSON 문자열, FormData 등 요청 본문을 담습니다.
- 3signal
검색어 변경이나 화면 이동 시 오래된 요청을 취소합니다.
async/await와 함께 Fetch 사용하기
우리는 7장에서 async/await 문법을 학습했습니다.
fetch() 함수가 Promise를 반환하므로, async/await와 함께 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있습니다.
async function fetchUserData(userId) {
try {
const response = await fetch(`http://localhost:4000/users/${userId}`);
if (!response.ok) {
throw new Error(`사용자 데이터를 불러오는 데 실패했습니다. ${response.status}`);
}
const userData = await response.json();
console.log("사용자 데이터:", userData);
return userData;
} catch (error) {
console.error("데이터 로딩 중 오류 발생:", error);
return null;
}
}
fetchUserData(1); // ID가 1인 사용자 데이터 가져오기
fetchUserData(999); // 존재하지 않는 사용자 (오류 처리 테스트)async/await를 사용하면 then().catch() 체인보다 에러 처리 및 코드 흐름을 파악하기 훨씬 용이합니다.
요청 취소: AbortController
AbortController는 네트워크를 빠르게 만드는 장치가 아니라, 현재 화면과 맞지 않는 비동기 결과를 끊는 안전장치다.
| 단계 | 하는 일 | 확인할 것 | 실패 신호 |
|---|---|---|---|
| create | AbortController 생성 | 요청마다 새 controller인가 | 여러 요청이 같은 signal 공유 |
| connect | fetch 옵션에 signal 전달 | 실제 요청과 연결됐는가 | abort를 해도 요청이 계속됨 |
| abort | unmount나 새 요청에서 취소 | cleanup에서 호출되는가 | stale 응답이 state 갱신 |
| catch | AbortError와 진짜 오류 분기 | 취소는 조용히 정리 | 취소를 실패 toast로 표시 |
Fetch 요청은 한 번 시작되면 취소하기 어려웠습니다.
AbortController는 Fetch 요청을 취소할 수 있는 메커니즘을 제공합니다.
이는 사용자가 페이지를 떠나거나, 검색어가 바뀌어 이전 요청이 불필요해질 때 유용합니다.
const controller = new AbortController();
const signal = controller.signal;
async function fetchWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
const data = await response.json();
console.log("데이터 로드 성공:", data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log("Fetch 요청이 취소되었습니다.");
} else {
console.error("Fetch 요청 오류:", error);
}
return null;
}
}
// 5초 후에 요청을 취소하는 예시
const longRunningFetch = fetchWithCancellation('http://localhost:4000/posts/1');
setTimeout(() => {
controller.abort(); // 요청 취소
}, 50); // 아주 짧은 시간 후에 취소하여 AbortError 발생 유도signal 옵션에 AbortController의 signal 프로퍼티를 전달하고, 필요할 때 controller.abort()를 호출하면 해당 요청이 취소됩니다.
CORS (Cross-Origin Resource Sharing)
Fetch API를 사용해 다른 도메인의 리소스에 접근할 때는 CORS (Cross-Origin Resource Sharing) 정책을 이해해야 합니다.
웹 브라우저는 보안상의 이유로 Same-Origin Policy (동일 출처 정책)을 따르며, 한 출처(Origin: 프로토콜, 호스트, 포트가 모두 동일한 경우)에서 로드된 페이지가 다른 출처 리소스에 접근하는 것을 제한합니다.
- 동일 출처:
http://example.com:8080/path와http://example.com:8080/anotherpath는 동일 출처. -
다른 출처
http://sub.example.com(다른 호스트)https://example.com(다른 프로토콜)http://example.com:3000(다른 포트)
다른 출처의 리소스에 접근하려면 서버에서 CORS 관련 HTTP 헤더(Access-Control-Allow-Origin 등)를 적절히 설정해 요청을 허용해야 합니다.
Fetch 요청의 mode 옵션을 cors(기본값) 또는 no-cors 등으로 설정해 브라우저 동작을 제어할 수 있으며, no-cors는 요청 전송은 허용하지만 스크립트의 응답 본문 접근은 제한합니다.
fetch는 HTTP 오류를 자동 예외로 만들지 않으므로, 응답 객체를 단계적으로 확인해야 합니다.
- 1
URL, method, headers, body, signal을 준비합니다.
- 2
연결 실패와 취소는 catch 블록에서 다룹니다.
- 3
response.ok와 status로 4xx, 5xx를 분기합니다.
- 4
json, text, blob 중 실제 응답 형식에 맞게 읽습니다.
Fetch 코드는 요청만 보내는 함수로 끝나지 않고, 화면의 로딩/성공/빈 결과/오류 상태와 함께 설계해야 실전에서 안정적입니다.
아래 다이어그램은 같은 요청 흐름을 UI 상태 전환 기준으로 다시 묶어 보여줍니다.
네트워크 요청은 response.ok 확인에서 끝나지 않습니다. 사용자가 보는 화면은 대기, 성공, 빈 결과, 실패, 취소 상태를 모두 가져야 합니다.
- idle요청 전
URL, method, headers, body, signal을 만들고 이전 요청 키와 비교합니다.
- loading대기 중
버튼 중복 클릭을 막고, 화면에는 현재 데이터가 갱신 중임을 표시합니다.
- success응답 성공
2xx 응답이면 본문을 파싱하고 최신 요청인지 확인한 뒤 상태를 반영합니다.
- empty빈 결과
정상 응답이지만 배열이 비었거나 표시할 값이 없으면 별도 안내를 표시합니다.
- error실패 처리
HTTP 오류, 네트워크 실패, AbortError를 구분해 재시도와 무시 조건을 나눕니다.
AJAX·Fetch 정리
이 절에서는 요청-응답 흐름을 오류 처리까지 포함해 재현 가능한 형태로 정리합니다.
이번 장에서는 웹 애플리케이션에서 서버와 비동기적으로 데이터를 주고받는 핵심 기술인 AJAX의 개념과, 그 현대적인 구현 표준인 Fetch API에 대해 심도 있게 학습했습니다.
fetch()는 Promise 기반으로 동작하며, HTTP 오류 응답만으로는 자동으로 reject되지 않습니다.
따라서 response.ok 확인, 요청 옵션(method, headers, body), async/await, AbortController, CORS 제약을 함께 점검해야 합니다.
Fetch API는 서버 통신을 구현할 때 가장 기본적으로 확인해야 할 브라우저 API입니다.
로컬 Mock API나 스테이징 API로 요청을 보내고, 응답 상태와 데이터를 화면에 표시하는 흐름을 반복해 검증해 보세요.
fetch는 네트워크 실패만 reject하고 HTTP 404/500은 응답 객체로 돌려준다. 그래서 ok/status 검사가 별도 단계가 된다.
| 구간 | 확인할 것 | 좋은 처리 | 놓치기 쉬운 점 |
|---|---|---|---|
| request | method, headers, body, credentials | 서버 계약을 코드에 드러냄 | GET body나 Content-Type 누락 |
| response | ok, status, headers | HTTP 오류를 직접 분기 | 404가 catch로 간다고 착각 |
| body | json/text/blob 파싱 | Content-Type에 맞춰 읽음 | 파싱 오류 처리 누락 |
| lifecycle | abort, retry, loading | UI 수명과 요청 수명 연결 | 언마운트 뒤 state 갱신 |
DNS·CORS·연결 실패와 서버가 보낸 400/500 응답은 다른 실패다. 분기 기준을 나누면 사용자 메시지도 달라진다.
| 경계 | 어디서 보이나 | 처리 기준 | 사용자에게 보일 것 |
|---|---|---|---|
| transport failure | Promise reject | catch에서 네트워크/차단 처리 | 연결 확인 또는 재시도 |
| HTTP error | response.ok false | 상태 코드와 오류 body 확인 | 검증 오류 또는 서버 오류 |
| parse failure | body 읽기 단계 | Content-Type과 JSON 형식 확인 | 응답 형식 오류 |
| auth/cors | credentials, preflight | 쿠키 포함과 서버 허용 확인 | 로그인 필요 또는 권한 문제 |
엔드포인트와 옵션을 정하고, HTTP 상태를 확인하고, 본문을 파싱하고, 화면 상태로 분기해야 안정적인 요청 코드가 된다.
| 단계 | 핵심 질문 | 대표 코드 | 위험 신호 |
|---|---|---|---|
| 1 요청 구성 | 서버 계약과 맞는가 | fetch(url, options) | 인증/헤더/body 누락 |
| 2 상태 확인 | 성공 응답인가 | if (!res.ok) throw | 404를 성공처럼 처리 |
| 3 본문 읽기 | 어떤 형식인가 | await res.json() | 빈 body 파싱 오류 |
| 4 UI 반영 | 성공/오류/취소가 나뉘나 | setState, catch, finally | loading이 남음 |