fetch()에서 await은 왜 두 번 사용할까?

김민재·2026년 1월 31일
post-thumbnail

서버에 데이터를 요청할 때, 다음과 같이 fetch()메서드를 사용합니다.

const response = await fetch("/url");

const text = await response.json()

그런데 분명 서버에 하는 요청은 한 번인데, 왜 response.json()에 await을 한 번 더 붙여주는 것일까요?

근본적인 원인은 서버가 데이터를 보낼 때 Header와 Body로 나누어서 보내기 때문입니다.

fetch() 을 호출하는 시점에서부터 데이터를 받아오는 시점까지 어떤 일이 발생하는지 살펴보면서 구체적인 이유를 알아보겠습니다.

1) 자바스크립트 엔진 -> 네트워크 프로세스 요청(첫 번째 await)

우선 fetch() API가 호출되면, 자바스크립트 엔진은 보안상 네트워크 프로세스와 직접 통신할 수 없으므로 렌더링 엔진을 거쳐 인자로 전달된 URL에 방문하여 데이터를 가져와달라는 요청을 하게 됩니다.

렌더링 엔진은 네트워크 프로세스에 IPC(Inter-Process Communication)를 통해 리소스 요청을 보낸 뒤, FetchLoader라는 객체를 만들어 저장합니다. FetchLoader는 다음과 같은 정보를 포함합니다.

  • ID: 요청에 대한 고유번호
  • priority: 요청의 우선순위
  • Resolver: 자바스크립트 엔진에 저장된 Promise 객체를 resolve 상태로 바꾸는 권한을 가진 메서드

자바스크립트 엔진은 이때 Pending 상태의 프로미스를 곧바로 반환하게 됩니다.


2. 네트워크 프로세스

네트워크 프로세스의 요청이 서버에 도달 완료하면, 서버는 다음과 같은 논리적 형태의 응답을 보냅니다.

// 헤더
HTTP/1.1 200 OK
Date: Fri, 30 Jan 2026 07:50:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 31
Connection: keep-alive

// 바디
{
  "status": "success",
  "id": 1
}

그런데, 실제로 전송되는 물리적인 형태는 다음과 같습니다.

  • H T T P (48 54 54 50): 상태 라인의 시작
  • ...: 중간 헤더 내용들
  • \r \n (0D 0A): 한 줄이 끝났음을 표시
  • \r \n \r \n (0D 0A 0D 0A): 헤더가 끝났음을 알리는 두 번의 엔터
  • { " s ... (7B 22 73 ...): 바디 데이터의 시작

브라우저 엔진은 들어오는 바이트를 하나하나 읽다가 0D 0A 0D 0A라는 신호를 발견하는 순간, 여기까지가 헤더이고 다음에 올 데이터는 바디라는 것을 인지합니다.

이 순간, 네트워크 프로세스는 메인 스레드에게 첫 번째 알림을 보내고, 렌더링 엔진에서는 저장되어 있던 FetchLoader의 Resolver를 실행하여 자바스크립트 엔진에 존재하는 Promise를 이행(fulfilled) 상태로 만듭니다.


그런데 왜 모든 데이터를 수신 완료했을 때가 아니라, 헤더 수신이 완료되었을 때 알림을 보내는 것일까요? 다음과 같은 이유가 있습니다.

빠른 의사결정 (Early Exit)

서버가 보낸 응답이 성공(200 OK)이 아닐 수도 있습니다. 만약 404(Not Found)나 500(Internal Server Error)이라면, 브라우저는 굳이 뒤따라오는 에러 메시지 바디를 다 받을 때까지 기다릴 필요가 없습니다. 헤더를 읽는 즉시 요청이 실패했다는 것을 인지하고 에러 처리나 다음 로직을 실행할 수 있기 때문입니다.

리소스 준비 (Content-Type & Length)

헤더에는 데이터의 정체인 Content-Type과 크기인 Content-Length가 들어 있습니다.

만약 1GB짜리 고화질 영상 데이터라면, 브라우저는 바디가 들어오기 전에 미리 스트리밍을 위한 메모리 버퍼를 준비하거나 너무 크다면 다운로드를 중단할지도 결정할 수 있습니다.

데이터가 이미지인지, JSON인지에 따라 어떤 파서(Parser)를 붙일지도 미리 계획할 수 있습니다.

스트리밍 처리 (The Power of Streaming)

가장 핵심적인 이유입니다. fetch는 데이터를 한꺼번에 받는 도구가 아니라, 흐름(Stream)으로 받는 도구입니다. 헤더 수신 직후 Promise가 해결되면, 개발자는 response.body.getReader() 를 통해 데이터가 들어올 때마다 실시간으로 처리할 수 있습니다. 100MB짜리 데이터를 다 받을 때까지 사용자를 기다리게 하는 것이 아니라, 들어온 10KB부터 즉시 화면에 그려줄 수 있는 거죠.


2. 두 번째 await

첫 번째 프로미스는 이행되었지만, 아직 본문의 데이터는 계속해서 받아오고 있는 중입니다.

response.json() 메서드를 사용한다면, 본문 데이터 스트림이 끝날 때까지 데이터를 모아서 파싱하고, 끝났다면(스트림을 모두 읽었다면) 여기서 반환한 Promise가 이행(resolved) 상태가 됩니다.


결론

fetch 사용 시 await을 두 번 사용하는 이유는, 데이터 처리의 효율성을 위해서입니다. 첫 번째 단계에서 헤더 정보를 먼저 받아 성공 여부나 데이터 형식을 파악함으로써 오류를 사전에 감지하거나 불필요한 리소스 낭비를 방지하고, 두 번째 단계에서 실제 바디 데이터를 읽어 들임으로써 네트워크 응답을 보다 안정적으로 제어할 수 있게 됩니다.

profile
넓이보다 깊이있게

2개의 댓글

comment-user-thumbnail
2026년 2월 3일

.then 체이닝을 하면 await 한 번만 써도 돼요

1개의 답글