fetch()에서 await을 두 번 쓰는 이유
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
들어가기
JavaScript에서 fetch()를 쓸 때 await이 두 번 등장하는 건 처음 보면 의아함.
이건 HTTP 통신 자체가 2단계 구조이기 때문이고, 알고 나면 굉장히 합리적인 설계다.
const response = await fetch(url); // 1단계: 서버 연결 + 헤더 수신
const data = await response.json(); // 2단계: 본문(body) 수신 + 파싱단계 | 반환 타입 | 하는 일 |
|---|---|---|
|
| 서버 연결, 응답 헤더 수신 |
| 파싱된 JS 객체 | 본문 스트림 수신 + JSON 파싱 |
두 번의
await= 두 번의 비동기 작업 (네트워크 연결 / 본문 읽기)
1단계 — await fetch(url)
서버에 요청을 보내고 응답 헤더가 도착할 때까지 기다린다.
이 시점에서 알 수 있는 것:
상태 코드 (
response.status→ 200, 404 등)헤더 정보 (
Content-Type,Content-Length등)응답 성공 여부 (
response.ok)
아직 본문(body)은 도착하지 않았다. 응답 헤더만 먼저 온 상태다.
2단계 — await response.json()
응답 본문을 스트림으로 끝까지 읽고, JSON으로 파싱한다.
본문 데이터는 네트워크를 통해 청크(chunk)로 도착하기 때문에, 전부 모아서 파싱하는 과정이 비동기다.
사용 가능한 본문 읽기 메서드는 여러 가지가 있다:
메서드 | 반환 타입 | 용도 |
|---|---|---|
| JS 객체 | JSON 데이터 |
| 문자열 | 텍스트, HTML |
| Blob | 이미지, 파일 |
| ArrayBuffer | 바이너리 데이터 |
| FormData | 폼 데이터 |
formData 가 있는지도 몰랐네
이 2단계 설계가 유용한 이유
상태 코드를 먼저 확인하고 분기 처리할 수 있다
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
// 본문을 읽지 않아도 에러 처리 가능
}
const data = await response.json();Content-Type에 따라 파싱 방법을 선택할 수 있다
const response = await fetch(url);
const contentType = response.headers.get('Content-Type');
if (contentType.includes('application/json')) {
const data = await response.json();
} else if (contentType.includes('text/')) {
const text = await response.text();
} else {
const blob = await response.blob();
}대용량 데이터를 스트림으로 처리할 수 있다
const response = await fetch(largeFileUrl);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// chunk 단위로 처리 — 메모리 절약
processChunk(value);
}.json()을 호출하지 않으면 어떻게 되나?
데이터는 어디에 있는가
서버 → [네트워크] → OS TCP 수신 버퍼 → 브라우저 내부 버퍼 → .json()으로 읽기
^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
약 64KB~ 제한 있음핵심 포인트 세 가지:
서버는 클라이언트의
.json()호출 여부와 무관하게 응답을 보낸다데이터는 OS/브라우저의 버퍼에 임시 보관된다
.json()은 이 버퍼에서 데이터를 꺼내서 JS 코드로 가져오는 동작이다
서버 입장에서 보면
상황 | 서버의 동작 |
|---|---|
작은 응답 (JSON 몇 KB) | 전부 보내고 연결 종료. 끝. |
큰 응답 (파일 다운로드 등) | 보내다가 클라이언트 버퍼가 차면 TCP 흐름 제어에 의해 일시 정지 |
클라이언트가 연결을 끊으면 | 서버에서 에러 발생 (broken pipe 등) |
버퍼가 가득 차면?
TCP 흐름 제어(Flow Control) 가 작동한다
응답이 작을 때 (수 KB ~ 수십 KB):
=> 서버 → 전부 보냄 → 버퍼에 다 들어감 → 문제 없음
응답이 클 때 (수 MB 이상):
=> 서버 → 계속 보냄 → 버퍼 가득 참 → TCP가 서버에게 "잠깐 멈춰!" 신호
→ 서버가 전송 속도를 줄이거나 일시 정지
권장 처리 방식
본문이 필요 없더라도 스트림을 명시적으로 정리하는 게 좋다:
const response = await fetch(url);
if (!response.ok) {
response.body?.cancel(); // 스트림을 명시적으로 닫음
throw new Error(`HTTP ${response.status}`);
}비유로 정리
await fetch(url)
→ 택배 기사가 문 앞에 도착 (배송장 = 헤더 확인 가능)
→ 하지만 아직 박스를 열지 않은 상태
await response.json()
→ 박스를 열고 내용물을 꺼내는 행위
정리
질문 | 답변 |
|---|---|
| HTTP 응답이 헤더와 본문 2단계로 나뉘기 때문 |
| 헤더 정보가 담긴 |
| 본문 스트림을 끝까지 읽고 JSON으로 파싱 |
| 서버는 이미 보냈고, 데이터는 OS/브라우저 버퍼에 있음 |
버퍼가 넘치면? | TCP 흐름 제어가 서버 전송을 자동으로 조절 |
본 글은 AI의 도움을 받아 작성되었습니다.