fetch()에서 await을 두 번 쓰는 이유

이 문서 주변 탐색

주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.

시리즈 흐름

이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.

관련 문서

같이 읽을 만한 관련 문서가 아직 없습니다.

이 문서를 참조하는 문서

이 문서를 참조하는 다른 문서가 아직 없습니다.

들어가기

JavaScript에서 fetch()를 쓸 때 await이 두 번 등장하는 건 처음 보면 의아함.
이건 HTTP 통신 자체가 2단계 구조이기 때문이고, 알고 나면 굉장히 합리적인 설계다.

const response = await fetch(url);   // 1단계: 서버 연결 + 헤더 수신
const data = await response.json();  // 2단계: 본문(body) 수신 + 파싱

단계

반환 타입

하는 일

await fetch()

Response 객체

서버 연결, 응답 헤더 수신

await response.json()

파싱된 JS 객체

본문 스트림 수신 + JSON 파싱

두 번의 await = 두 번의 비동기 작업 (네트워크 연결 / 본문 읽기)

1단계 — await fetch(url)

서버에 요청을 보내고 응답 헤더가 도착할 때까지 기다린다.

이 시점에서 알 수 있는 것:

  • 상태 코드 (response.status → 200, 404 등)

  • 헤더 정보 (Content-Type, Content-Length 등)

  • 응답 성공 여부 (response.ok)

아직 본문(body)은 도착하지 않았다. 응답 헤더만 먼저 온 상태다.

2단계 — await response.json()

응답 본문을 스트림으로 끝까지 읽고, JSON으로 파싱한다.

본문 데이터는 네트워크를 통해 청크(chunk)로 도착하기 때문에, 전부 모아서 파싱하는 과정이 비동기다.

사용 가능한 본문 읽기 메서드는 여러 가지가 있다:

메서드

반환 타입

용도

.json()

JS 객체

JSON 데이터

.text()

문자열

텍스트, HTML

.blob()

Blob

이미지, 파일

.arrayBuffer()

ArrayBuffer

바이너리 데이터

.formData()

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()
→ 박스를 열고 내용물을 꺼내는 행위

정리

질문

답변

await을 왜 두 번 쓰나?

HTTP 응답이 헤더와 본문 2단계로 나뉘기 때문

fetch()는 뭘 반환하나?

헤더 정보가 담긴 Response 객체 (본문은 아직 미수신)

.json()은 뭘 하나?

본문 스트림을 끝까지 읽고 JSON으로 파싱

.json() 안 쓰면?

서버는 이미 보냈고, 데이터는 OS/브라우저 버퍼에 있음

버퍼가 넘치면?

TCP 흐름 제어가 서버 전송을 자동으로 조절

🤖

본 글은 AI의 도움을 받아 작성되었습니다.