배경
✌️
개발 •  • 읽는데 4분 소요

JavaScript fetch 메서드는 왜 2번 await 해야만 할까?

JavaScript fetch에서 데이터를 얻기 위해 두 번의 await이 필요한 이유에 대해 알아봅니다.

#JavaScript


일반적인 JavaScript 코드에서 fetch 함수를 사용할 때, 우리는 다음과 같이 코드를 작성하곤 합니다.

// 첫 번째 await
let response = await fetch('/url');

// 두 번째 await
let data = await response.json();

우리가 원하는 데이터까지 접근하기 위해서는 await 키워드를 두 번 사용한 것을 볼 수 있죠. 첫 번째 await 은 그 이유가 짐작이 갑니다. 인자로 넘긴 엔드 포인트에 직접 요청을 보내고 응답을 받아오기까지 오랜 시간이 걸릴 수 있으니까요. 요청을 보낸 서버가 한국에 있을 수도 있고, 지구 반대편의 미국에 있을 수도 있으니까요.

그렇지만 두 번째 await 의 필요성은 의아하게 느껴집니다. 왜냐하면 JSON 데이터를 파싱하는 작업은 그렇게 오래 걸리는 작업이 아니거든요. 우리가 일반적으로 JSON 데이터를 파싱할 때는 동기 메서드인 JSON.parse() 를 사용하는 걸 생각해 보면 더욱 그렇습니다.

// 이렇게 간단히 할 수 있는 작업인데 왜 Promise를 리턴할까?
let data = JSON.parse('{ "name": "John" }');

그렇다면 response.json() 은 왜 Promise 를 리턴하는 걸까요? 오늘은 그 이유에 대해 알아보려고 합니다.

MDN 정의 살펴보기

멀리 갈 필요 없이 MDN에서 fetch 메서드의 설명을 살펴보고자 합니다.

fetch

fetch() 메서드는 하나의 필수 매개변수로 가져오려는 리소스 경로를 받습니다. 반환 값은 해당 요청에 대한 Response 로 이행하는 Promise인데, 서버가 헤더를 포함한 응답을 하는 순간 이행합니다. 이는 서버가 HTTP 오류 응답 코드로 응답해도 이행한다는 뜻입니다.

여기서 우리는 답을 바로 얻을 수 있는데요. fetch 메서드가 이행(resolve) 되는 순간은 서버로부터 HTTP 헤더를 수신했을 때 라는 점입니다. 이는 곧 첫 번째 await 이 이행되는 것이 바디(body)까지 수신되었다는 것을 의미하지는 않는다는 말이죠.

간단한 이미지 HTTP 메시지의 예제

HTTP 메시지는 크게 헤더와 바디의 순서로 구성되어 있습니다. 헤더에는 메시지의 메타데이터를 담고 있으며, 바디는 우리가 받고자 하는 실제 데이터를 담고 있죠.

헤더와 바디는 공백 문자열로 구분되어 있어, 브라우저는 항상 헤더를 먼저 수신한 다음 바디를 수신합니다. 이 때문에 fetch 메서드는 헤더만 수신된 상태바디까지 모두 수신된 상태를 구별할 수 있는 것이죠. 그런데 이 두 개의 상태를 구별하는 것이 왜 필요할까요?

이러한 상태를 구별하는 것이 유용한 경우는 바로 바디의 크기가 너무 커서 하나의 응답으로 실어 보낼 수 없거나, 서버에서 스트리밍을 통해 데이터를 전송하는 경우입니다.

예제 코드로 알아보기

이를 조금 더 쉽게 이해할 수 있도록 예제를 보여드리겠습니다. 전체 예제 코드는 이 곳에서 확인할 수 있습니다.

예제 코드에서는 간단한 JSON 데이터를 반환하는 Node.js 서버를 구성하고, 클라이언트에서는 이를 fetch 로 요청하는 코드가 작성되어 있습니다.

이때 서버에서는 응답을 반환할 때, Node.js의 스트림을 이용해 한 번에 한 바이트씩 바디를 구성해 클라이언트에 전송하도록 구현했습니다. 마치 ChatGPT가 답변을 작성하는 것처럼요!

우선 클라이언트에서 다음과 같은 코드를 통해 서버에 요청을 보내는 코드를 작성해 보았습니다.

async function example1() {
  console.log('making request');
  let response = await fetch('/json');
  console.log('got response headers, now waiting for the body');
  let data = await response.json();
  console.log('turned the JSON in an object');
  console.log(data);
}

예제

이를 실행시키면 다음과 같은 결과를 볼 수 있는데요.

응답 fetch의 헤더는 응답이 모두 오기 전에도 확인 가능한 것을 볼 수 있다

여기서 우리는 HTTP 응답의 크기가 점점 늘어나는 것을 보며 스트림을 통해 실시간으로 데이터가 전송되는 것을 확인할 수 있습니다.

또한 바디를 모두 수신하기도 전에 이미 HTTP 헤더에 포함된 상태 코드 200이 수신되어 클라이언트에서 확인이 가능하다는 것을 볼 수 있죠. 이를 통해 fetch 메서드가 이행되는 시점은 헤더를 수신한 시점이라는 것을 다시 한번 확인할 수 있습니다.

예제 코드를 조금 더 발전시켜, 스트림을 받아오는 즉시 화면에 출력하는 코드를 작성해 보겠습니다. fetch 응답의 body 속성은 ReadableStream 타입으로, 데이터를 스트림 방식으로 읽을 수 있는데 각 스트림에 await 을 적용할 수 있도록 for await 문을 사용하였습니다.

async function example2() {
  outputBox.textContent = '';
  let response = await fetch('/json');
  const decoder = new TextDecoder('utf-8');
  for await (const value of response.body) {
    const chunk = decoder.decode(value);
    outputBox.textContent += chunk;
  }
}

실행 실시간으로 스트림 결과물을 확인해 보기

그러면 위와 같이 실시간으로 받아온 스트림이 화면에 출력되는 것을 확인할 수 있습니다. 이를 통해 우리는 두 번째 await 이 완료된 시점에서야 바디를 모두 수신한 상태라는 것을 확인할 수 있습니다.

마무리

앞선 코드를 다시 살펴보면,

let response = await fetch('/url');

// 이 시점에서
// 1. 클라이언트에서는 HTTP 헤더를 수신한 상태
// 2. 응답의 본문은 서버에서 아직 생성 중이거나 전송 중인 상태

let data = await response.json();

// 이 시점에서
// 1. 클라이언트는 응답의 본문을 수신한 상태
// 2. 클라이언트는 받은 응답을 JSON 형태로 파싱하여 JavaScript 객체로 변환함

응답의 본문이 도착하기 전에 헤더만 수신할 수 있으므로, HTTP 상태 코드나 특정 헤더에 따라 본문 전체를 기다리거나 읽지 않도록 할 수 있다는 점에서 유용하게 사용할 수 있겠네요. HTTP 상태 코드가 404나 500이라면, 굳이 본문을 읽는 데 시간을 낭비할 필요가 없으니까요.

참고 자료

이 포스트가 유익하셨다면?




프로필 사진

👨‍💻 정종윤

글 쓰는 것을 좋아하는 프론트엔드 개발자입니다. 온라인에서는 재그지그라는 닉네임으로 활동하고 있습니다.


Copyright © 2024, All right reserved.

Built with Gatsby