fetch API로 GET·POST 요청, 응답 상태 코드 처리, JSON 직렬화, 요청 헤더와 인증, 로딩·오류 상태 UI 패턴, AbortController로 요청 취소까지 실전 HTTP 통신을 완성합니다.
🎯 학습 목표
이 강을 마치면 다음을 할 수 있습니다.
fetch로 GET 요청을 보내고 JSON 응답을 파싱할 수 있습니다.fetch로 POST 요청을 보내고 데이터를 서버에 전송할 수 있습니다.- HTTP 상태 코드를 확인하고 오류를 적절히 처리할 수 있습니다.
- 로딩·성공·오류 세 가지 UI 상태를 관리할 수 있습니다.
🌐 fetch API 기초
이 섹션에서는 브라우저 내장 fetch API의 기본 사용법을 살펴보겠습니다.
fetch API는 브라우저에 내장된 HTTP 통신 인터페이스입니다. 과거에는 XMLHttpRequest(XHR)를 사용했지만, fetch가 더 직관적인 Promise 기반 API를 제공해 현재 표준이 되었습니다. fetch는 URL을 인수로 받아 Promise를 반환합니다. 기본적으로 GET 요청을 보냅니다.
// 기본 GET 요청
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => {
console.log(response.status); // 200
console.log(response.ok); // true (200-299)
console.log(response.statusText); // 'OK'
return response.json(); // 응답 본문을 JSON으로 파싱
})
.then(user => {
console.log(user.name); // 'Leanne Graham'
})
.catch(err => {
// 네트워크 오류(인터넷 끊김 등)만 catch
// HTTP 오류(404, 500)는 catch 안 됨!
console.error('네트워크 오류:', err);
});
// async/await 방식 (권장)
async function getUser(id) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/' + id);
// fetch는 HTTP 오류(404, 500)에도 reject하지 않음
// response.ok를 반드시 확인해야 함
if (!response.ok) {
throw new Error('HTTP 오류: ' + response.status);
}
const user = await response.json();
return user;
} catch (error) {
console.error('오류:', error.message);
throw error;
}
}
const user = await getUser(1);
console.log(user.name);
fetch에서 가장 중요한 주의사항: fetch는 HTTP 오류 상태 코드(404, 500 등)에도 Promise를 reject하지 않습니다. 네트워크 연결 자체가 끊어진 경우만 reject됩니다. 따라서 response.ok(200~299이면 true)를 반드시 확인하고, false이면 수동으로 오류를 throw해야 합니다. response.json()도 Promise를 반환하므로 await가 필요합니다. 응답 본문을 읽는 메서드는 .json() 외에도 .text()(문자열), .blob()(파일), .arrayBuffer()(바이너리)가 있습니다.
📤 POST 요청 — 데이터 전송
이 섹션에서는 서버에 데이터를 전송하는 POST 요청 작성법을 살펴보겠습니다.
GET 요청은 데이터를 조회할 때, POST 요청은 새로운 데이터를 생성하거나 서버에 데이터를 전송할 때 사용합니다. fetch의 두 번째 인수인 options 객체로 메서드, 헤더, 본문을 설정합니다.
// POST 요청 — JSON 데이터 전송
async function createUser(userData) {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // JSON 전송 시 필수
},
body: JSON.stringify(userData), // JS 객체 → JSON 문자열
});
if (!response.ok) {
throw new Error('생성 실패: ' + response.status);
}
return response.json(); // 서버가 반환한 생성된 데이터
}
const newUser = await createUser({
name: '홍길동',
email: 'hong@example.com',
username: 'hong'
});
console.log(newUser.id); // 서버가 할당한 ID
// PUT — 전체 데이터 수정
async function updateUser(id, data) {
const response = await fetch('/api/users/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('수정 실패');
return response.json();
}
// PATCH — 일부 데이터 수정
async function patchUser(id, changes) {
const response = await fetch('/api/users/' + id, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
});
if (!response.ok) throw new Error('수정 실패');
return response.json();
}
// DELETE
async function deleteUser(id) {
const response = await fetch('/api/users/' + id, {
method: 'DELETE',
});
if (!response.ok) throw new Error('삭제 실패');
// DELETE 응답에 본문이 없을 수 있음
return response.status === 204 ? null : response.json();
}
REST API에서 HTTP 메서드의 역할을 정리하면 이렇습니다. GET: 조회, POST: 생성, PUT: 전체 수정, PATCH: 일부 수정, DELETE: 삭제. Content-Type: application/json 헤더는 서버에게 전송 데이터가 JSON임을 알립니다. 이 헤더가 없으면 서버가 본문을 제대로 파싱하지 못할 수 있습니다. body는 문자열이어야 하므로 JSON.stringify()로 변환합니다.
🔐 요청 헤더와 인증
이 섹션에서는 API 인증에 필요한 헤더를 설정하는 방법을 살펴보겠습니다.
대부분의 실제 API는 인증이 필요합니다. 가장 일반적인 방식은 Bearer 토큰을 Authorization 헤더에 담아 전송하는 것입니다.
// Authorization 헤더로 인증
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('accessToken');
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token, // JWT 토큰
...options.headers, // 추가 헤더
},
});
// 401: 인증 만료
if (response.status === 401) {
// 토큰 갱신 후 재시도 로직
await refreshToken();
return fetchWithAuth(url, options);
}
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}
// 래퍼 함수로 편리하게 사용
const api = {
get: (url) => fetchWithAuth(url),
post: (url, data) => fetchWithAuth(url, { method: 'POST', body: JSON.stringify(data) }),
put: (url, data) => fetchWithAuth(url, { method: 'PUT', body: JSON.stringify(data) }),
delete: (url) => fetchWithAuth(url, { method: 'DELETE' }),
};
// 사용
const users = await api.get('/api/users');
const newUser = await api.post('/api/users', { name: '홍길동' });
⏳ 로딩·오류 UI 상태 관리
이 섹션에서는 API 호출 중 사용자에게 적절한 피드백을 제공하는 로딩·성공·오류 상태 패턴을 살펴보겠습니다.
실제 앱에서 API 호출은 세 가지 상태를 거칩니다. 로딩 중(Loading): 요청 중 스피너 표시. 성공(Success): 데이터 렌더링. 오류(Error): 오류 메시지 표시. 이 세 가지를 모두 처리해야 좋은 UX를 제공할 수 있습니다.
// HTML:
// <div id="loading" class="hidden">로딩 중...</div>
// <div id="error" class="hidden"></div>
// <ul id="user-list"></ul>
const loadingEl = document.querySelector('#loading');
const errorEl = document.querySelector('#error');
const userListEl = document.querySelector('#user-list');
async function loadUsers() {
// 로딩 상태 시작
loadingEl.classList.remove('hidden');
errorEl.classList.add('hidden');
userListEl.innerHTML = '';
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error('서버 오류: ' + response.status);
}
const users = await response.json();
// 성공: 데이터 렌더링
users.forEach(user => {
const li = document.createElement('li');
li.textContent = user.name + ' (' + user.email + ')';
userListEl.append(li);
});
} catch (error) {
// 오류 표시
errorEl.textContent = '데이터를 불러오지 못했습니다: ' + error.message;
errorEl.classList.remove('hidden');
console.error(error);
} finally {
// 로딩 종료 (성공/실패 모두)
loadingEl.classList.add('hidden');
}
}
loadUsers();
🚫 AbortController — 요청 취소
이 섹션에서는 진행 중인 fetch 요청을 취소하는 방법을 살펴보겠습니다.
검색 자동완성처럼 사용자가 빠르게 타이핑하면 이전 요청이 불필요해집니다. AbortController로 진행 중인 요청을 취소할 수 있습니다.
let controller = null;
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', async (e) => {
const query = e.target.value.trim();
if (!query) return;
// 이전 요청 취소
if (controller) controller.abort();
controller = new AbortController();
try {
const response = await fetch('/api/search?q=' + encodeURIComponent(query), {
signal: controller.signal // AbortController 연결
});
if (!response.ok) throw new Error('검색 실패');
const results = await response.json();
renderResults(results);
} catch (error) {
if (error.name === 'AbortError') {
// 취소는 오류가 아님 — 무시
return;
}
console.error('검색 오류:', error.message);
}
});
// 타임아웃과 AbortController 결합
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('요청 타임아웃 (' + ms + 'ms)');
}
throw error;
}
}
📝 핵심 요약
26강에서 배운 내용을 정리해 보겠습니다.
| 개념 | 핵심 내용 |
|---|---|
| 기본 GET | fetch(url) → response.ok 확인 → response.json() |
| POST | method: 'POST', Content-Type: application/json, body: JSON.stringify(data) |
| 오류 처리 | fetch는 HTTP 오류에서 reject 안 함 → response.ok 수동 확인 필수 |
| 인증 | Authorization: Bearer <token> 헤더 |
| 로딩 상태 | 로딩 시작 → try/catch → finally에서 로딩 종료 |
| 요청 취소 | AbortController + signal 옵션 |
다음 강인 27강 — 로컬 스토리지에서는 브라우저에 데이터를 영구 저장하는 방법을 배웁니다. 로그인 상태, 사용자 설정, 장바구니처럼 서버 없이 브라우저에 저장해야 하는 데이터는 localStorage와 sessionStorage로 관리합니다!
관련 주제
- fetch API
- GET POST 요청
- HTTP 상태 코드
- JSON 파싱
- AbortController
- 로딩 에러 상태
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 자바스크립트 기초 30강 — 웹을 움직이는 언어
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
자바스크립트 기초 30강 — 웹을 움직이는 언어
이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
불러오는 중...
