Fetch API 심화 — Streaming, Request-Response, Interceptor 패턴
Fetch API는 단순히
fetch(url).then(r => r.json())만 하는 것이 아닙니다. Request/Response 객체를 깊이 이해하면 스트리밍 다운로드, 인터셉터, 캐시 제어까지 가능합니다.
Fetch 기본 복습
// GET 요청
const response = await fetch("/api/users");
const data = await response.json();
// POST 요청
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "정훈", age: 25 }),
});
fetch가 에러를 던지지 않는 함정
// 404, 500 등 HTTP 에러에서도 fetch는 reject하지 않음!
const response = await fetch("/api/not-found");
console.log(response.ok); // false
console.log(response.status); // 404
// 에러가 아님 — response는 정상적으로 반환됨
// 네트워크 에러(서버 다운, CORS 등)만 reject
try {
await fetch("https://unreachable.example.com");
} catch (err) {
console.log("네트워크 에러:", err);
}
Request 객체
// Request를 직접 생성
const request = new Request("/api/data", {
method: "POST",
headers: new Headers({
"Content-Type": "application/json",
Authorization: "Bearer token123",
}),
body: JSON.stringify({ key: "value" }),
mode: "cors",
credentials: "include", // 쿠키 포함
cache: "no-cache",
});
// fetch에 전달
const response = await fetch(request);
// Request 복제 (한번 사용한 Request는 재사용 불가)
const cloned = request.clone();
Response 객체
const response = await fetch("/api/data");
// 상태 정보
console.log(response.status); // 200
console.log(response.statusText); // "OK"
console.log(response.ok); // true (200-299)
console.log(response.url); // 최종 URL (리다이렉트 후)
console.log(response.redirected); // 리다이렉트 여부
// 헤더 접근
console.log(response.headers.get("Content-Type"));
response.headers.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// 본문 읽기 (한 번만 가능!)
const json = await response.json();
// await response.text(); // 이미 읽었으므로 TypeError
본문 읽기 메서드
const response = await fetch(url);
// 텍스트
const text = await response.text();
// JSON
const json = await response.json();
// Blob (이미지, 파일)
const blob = await response.blob();
// ArrayBuffer (바이너리)
const buffer = await response.arrayBuffer();
// FormData
const formData = await response.formData();
스트리밍 응답 — 다운로드 진행률
async function downloadWithProgress(url) {
const response = await fetch(url);
const contentLength = response.headers.get("Content-Length");
const total = parseInt(contentLength, 10);
const reader = response.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
const progress = ((received / total) * 100).toFixed(1);
console.log(`다운로드 진행: ${progress}%`);
}
// 청크 합치기
const blob = new Blob(chunks);
return blob;
}
스트리밍 텍스트 — ChatGPT 스타일
async function streamText(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
result += chunk;
// 실시간으로 UI에 추가
updateUI(chunk);
}
return result;
}
인터셉터 패턴 — fetch 래퍼
Axios의 인터셉터와 비슷한 기능을 구현할 수 있습니다.
class HttpClient {
constructor(baseURL = "") {
this.baseURL = baseURL;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
// 요청 인터셉터 등록
onRequest(fn) {
this.requestInterceptors.push(fn);
}
// 응답 인터셉터 등록
onResponse(fn) {
this.responseInterceptors.push(fn);
}
async fetch(url, options = {}) {
let config = { url: this.baseURL + url, ...options };
// 요청 인터셉터 실행
for (const interceptor of this.requestInterceptors) {
config = await interceptor(config);
}
let response = await fetch(config.url, config);
// 응답 인터셉터 실행
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
return response;
}
}
// 사용
const api = new HttpClient("https://api.example.com");
// 인증 토큰 자동 추가
api.onRequest((config) => {
config.headers = {
...config.headers,
Authorization: `Bearer ${getToken()}`,
};
return config;
});
// 401 에러 시 토큰 갱신
api.onResponse(async (response) => {
if (response.status === 401) {
await refreshToken();
// 재요청 로직
}
return response;
});
// 에러 응답 자동 처리
api.onResponse(async (response) => {
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response;
});
재시도 패턴
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// 5xx 에러는 재시도
if (response.status >= 500 && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // 지수 백오프
await new Promise((r) => setTimeout(r, delay));
continue;
}
return response; // 4xx 에러는 재시도하지 않음
} catch (err) {
if (attempt === maxRetries) throw err;
const delay = Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, delay));
}
}
}
캐시 제어
// 캐시 모드 옵션
await fetch(url, { cache: "default" }); // 기본 브라우저 캐시
await fetch(url, { cache: "no-store" }); // 캐시 완전 무시
await fetch(url, { cache: "no-cache" }); // 서버에 재검증 요청
await fetch(url, { cache: "force-cache" }); // 캐시 우선
await fetch(url, { cache: "reload" }); // 캐시 무시, 새로 요청
// 조건부 요청
await fetch(url, {
headers: {
"If-None-Match": etag, // ETag 기반
"If-Modified-Since": lastModified, // 날짜 기반
},
});
fetch vs Axios
| 기능 | fetch | Axios |
|---|---|---|
| 브라우저 내장 | O | X (설치 필요) |
| HTTP 에러 자동 reject | X | O |
| 요청/응답 인터셉터 | 직접 구현 | 내장 |
| 타임아웃 | AbortSignal | timeout 옵션 |
| 진행률 | ReadableStream | onUploadProgress |
| JSON 자동 변환 | X (.json() 호출) | O |
**기억하기 **: fetch는 HTTP 에러(4xx, 5xx)에서 reject하지 않습니다.
response.ok를 반드시 확인해야 합니다. 스트리밍이 필요하면response.body.getReader()를, 인터셉터가 필요하면 fetch 래퍼를 만들어 사용하면 됩니다.
댓글 로딩 중...