자바스크립트 중급 #6 fetch API와 에러 처리
#5 옵셔널 체이닝과 nullish 병합 다음, 이번엔 자바스크립트로 외부 세상과 통신하는 도구를 정리합니다. 브라우저와 Node 양쪽에서 동작하는 표준 fetch API입니다.
fetch 기본 #
const response = await fetch('https://api.example.com/users');
const users = await response.json();
console.log(users);세 줄로 끝납니다. fetch는 Promise를 반환해서 — await로 풉니다(#2 비동기 입문에서 본 패턴).
fetch의 결과는 Response 객체입니다. 응답 본문을 어떻게 해석할지에 따라 메서드가 달라요.
await response.json(); // JSON 파싱
await response.text(); // 텍스트 그대로
await response.blob(); // 바이너리 (이미지/파일 등)
await response.arrayBuffer(); // ArrayBuffer (저수준)
await response.formData(); // FormData
함정 1 — 4xx/5xx는 catch로 안 잡힌다 #
자바스크립트의 fetch는 HTTP 상태 코드가 4xx/5xx 여도 reject 하지 않습니다. 네트워크 자체가 실패했을 때만 reject 합니다.
try {
const res = await fetch('/api/not-found');
// 상태가 404 인데도 여기까지 옴
console.log(res.status); // 404
} catch (err) {
// 네트워크 실패가 아니라면 안 들어옴
}이 동작이 다른 언어/라이브러리(axios 등)와 다르고, 혼란을 자주 일으켜요. 상태 검사를 직접 해야 합니다.
const res = await fetch('/api/users');
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();res.ok는 상태가 2xx 범위면 true. 이 검사를 빠뜨리면 4xx 응답을 그대로 JSON 파싱하려다 또 다른 에러로 이어져요.
메서드와 본문 — POST/PUT/DELETE #
기본은 GET. 다른 메서드를 쓰려면 옵션 객체를 두 번째 인자로 줍니다.
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: '커티스', age: 30 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const created = await res.json();세 가지 핵심:
method— 기본 ‘GET’, POST/PUT/PATCH/DELETE 등headers—Content-Type으로 본문 형식 명시body— 보낼 본문. 객체는 직접 못 보내고JSON.stringify로 문자열화
FormData 보내기 #
파일 업로드처럼 multipart 본문이 필요하면:
const formData = new FormData();
formData.append('name', '커티스');
formData.append('avatar', fileInput.files[0]);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
// Content-Type 은 일부러 안 적음 — 브라우저가 자동 설정
});FormData를 보낼 때는 Content-Type을 직접 적지 않습니다. 브라우저가 boundary를 포함한 정확한 헤더를 자동으로 만들어 줍니다.
헤더와 인증 #
const token = localStorage.getItem('token');
const res = await fetch('/api/me', {
headers: {
'Authorization': `Bearer ${token}`,
},
});쿠키를 함께 보내려면 credentials 옵션:
const res = await fetch('/api/me', {
credentials: 'include', // 다른 origin 에도 쿠키 보냄
});기본값은 'same-origin' (같은 도메인일 때만 쿠키). 외부 API가 쿠키 인증을 쓰면 'include'가 필요합니다.
쿼리 파라미터 — URLSearchParams
#
URL에 쿼리 문자열을 붙일 때 직접 문자열을 만드는 건 위험합니다. URLSearchParams가 인코딩까지 처리해 줍니다.
const params = new URLSearchParams({
q: '검색어 with spaces',
page: '1',
sort: 'date',
});
const res = await fetch(`/api/posts?${params}`);
// /api/posts?q=%EA%B2%80%EC%83%89%EC%96%B4+with+spaces&page=1&sort=date
+, &, 한글, 공백 모두 자동으로 URL 인코딩됩니다.
AbortController — 요청 취소 #
오래 걸리는 요청을 도중에 취소하고 싶을 때 사용합니다.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5초 뒤 취소
try {
const res = await fetch('/api/slow', {
signal: controller.signal,
});
console.log(await res.json());
} catch (err) {
if (err.name === 'AbortError') {
console.log('취소됨');
} else {
throw err;
}
}이게 #2 비동기 입문 에서 살짝 언급한 timeout의 모던 답입니다. Promise.race + 타이머보다 깔끔합니다.
AbortSignal.timeout() — ES2022
#
자주 사용하는 패턴이라 짧은 형태도 추가됐습니다.
const res = await fetch('/api/slow', {
signal: AbortSignal.timeout(5000),
});5초 초과 시 자동으로 abort. AbortController를 직접 만들 필요 없습니다.
동시 요청 취소 — 컴포넌트 언마운트 패턴 #
리액트 컴포넌트가 언마운트됐는데 그동안 진행 중이던 fetch 결과가 늦게 도착하면, 없어진 컴포넌트에 setState를 호출해 경고가 나요. AbortController가 표준 답입니다.
function UserPage({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // cleanup
}, [id]);
// ...
}return () => controller.abort()가 핵심. 언마운트나 의존성 변경 시 진행 중이던 fetch가 취소됩니다.
스트리밍 응답 — for await
#
큰 응답을 chunk 단위로 받고 싶을 때.
const res = await fetch('/api/large');
for await (const chunk of res.body) {
console.log(chunk); // Uint8Array 조각
}res.body는 ReadableStream. for await (#3 이터레이터와 제너레이터)로 chunk를 차례로 받아옵니다. AI streaming 응답, 큰 파일, 서버-전송 이벤트 같은 경우에 만나요.
Node 에서의 fetch #
옛날 Node에서는 fetch가 없어서 node-fetch 패키지를 설치해야 했습니다. Node 18부터는 fetch가 빌트인입니다. 추가 설치 없이 바로 사용합니다.
const res = await fetch('https://api.example.com/users');
const users = await res.json();스크립트, CLI 도구, 서버사이드 모두 같은 API로 통신할 수 있습니다.
로깅과 재시도 — 작은 wrapper #
여러 곳에서 같은 패턴(상태 검사, 에러 로깅)이 반복되면 wrapper 함수를 만드는 게 깔끔합니다.
async function api(url, options = {}) {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${url}`);
}
return res.json();
}
// 사용처
const users = await api('/api/users');
const created = await api('/api/users', {
method: 'POST',
body: JSON.stringify({ name: '커티스' }),
});이걸 더 발전시키면 — 재시도, 타임아웃, 인증 토큰 자동 추가 등을 한 군데서 관리할 수 있습니다. 큰 앱이라면 결국 비슷한 wrapper가 자기만의 모양으로 만들어집니다.
라이브러리를 쓸까, fetch 만으로 갈까 #
axios, ky, ofetch 같은 fetch 위에 얹는 라이브러리도 많습니다. 각자의 트레이드오프:
| 옵션 | 장점 | 단점 |
|---|---|---|
빌트인 fetch | 의존성 0, 표준 | 4xx 검사 직접, 보일러플레이트 |
| ky | 작고 모던, fetch 기반 | 의존성 추가 |
| axios | 풍부한 기능, 옛날부터 표준 | 무겁고 커스텀 동작 많음 |
| TanStack Query | 캐싱/동기화까지 | 학습 비용 |
작은 프로젝트, 라이브러리, 학습 코드는 fetch 만으로 충분합니다. 큰 앱이면 보통 wrapper 또는 데이터 페칭 라이브러리(TanStack Query) 와 합쳐지는 흐름입니다.
마무리 #
이번 글에서 정리한 내용:
fetch는 Promise 반환, 응답은Response객체.json(),.text(),.blob()로 본문 해석- 4xx/5xx는 자동으로 throw 안 됨 —
res.ok직접 검사 - POST는 method/headers/body 옵션 +
JSON.stringify - FormData는 Content-Type 자동
URLSearchParams로 안전한 쿼리 인코딩AbortController/AbortSignal.timeout()로 취소for await (chunk of res.body)로 스트리밍- Node 18+ 도 빌트인 fetch
- 작은 wrapper / TanStack Query 같은 라이브러리는 큰 앱에서 가치
다음 글(#7 JSON 다루기와 직렬화)에서는 자바스크립트 데이터를 JSON으로 주고받을 때 자주 만나는 것들 — parse/stringify의 옵션, Date/특수값 처리, 깊은 복사 트릭의 함정까지 정리합니다.