Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
작성일:2026.03.19|조회수:13

다운로드 화면에서 진행 막대가 조금씩 차오르면 이상하게 안심된다. 반대로 스피너만 계속 돌면 파일이 오는 중인지, 서버가 고민 중인지, 내 인생이 잠깐 멈춘 건지 알 수 없다. 사용자는 둘 다 “다운로드 중”이라고 느끼지만, 내부적으로는 꽤 다른 상황일 수 있다.
진행률 계산 자체는 복잡한 마법이 아니다. 지금까지 받은 바이트와 전체 바이트를 알면 된다. 문제는 HTTP가 언제나 전체 크기를 알려주지는 않는다는 점이다. 이 부록에서는 Content-Length, chunked transfer, stream reader를 연결해서 다운로드 진행률이 언제 계산 가능하고 언제 불가능한지 정리한다.
다운로드 진행률의 기본 공식
다운로드 진행률은 보통 다음과 같은 방식으로 계산된다: progress = 받은 바이트 / 전체 바이트
예를 들어 10MB짜리 파일을 다운로드한다고 가정해 보자. 다운로드가 진행되면서 브라우저는 현재까지 받은 데이터 양을 계속 기록한다.
전체 크기 = 10MB
현재 받은 데이터 = 5MB
이 경우 진행률은 단순히 5 / 10 = 50%와 같이 계산할 수 있다. 이 값이 바로 우리가 화면에서 보는 퍼센트다. 즉 다운로드 진행률을 계산하는 데 필요한 정보는 사실 두 가지뿐이다. 지금까지 받은 데이터의 양과, 전체 데이터의 크기다.
여기서 중요한 점이 하나 있다. 이 공식이 성립하려면 반드시 전체 데이터 크기를 알고 있어야 한다는 것이다.
HTTP는 전체 크기를 알려줄 수도 있고, 알려주지 않을 수도 있다
TTP 응답에는 Content-Length라는 헤더가 존재한다. 이 헤더는 응답 본문의 전체 크기를 바이트 단위로 알려준다. 예를 들어 서버가 다음과 같은 응답을 보냈다고 생각해 보자.
Content-Length: 10485760이 값은 응답 본문이 약 10MB라는 뜻이다. 브라우저는 이 값을 기준으로 다운로드가 얼마나 진행되었는지를 계산할 수 있다. 실제로는 네트워크에서 데이터가 조금씩 도착할 때마다 받은 바이트 수를 누적하고, Content-Length 값과 비교해 현재 진행률을 계산한다. 즉 내부적으로는 대략 다음과 같은 계산이 반복된다.
progress = receivedBytes / contentLength브라우저는 이 값을 이용해 진행 막대를 업데이트하거나 퍼센트 값을 표시한다. 우리가 보는 다운로드 UI는 사실 이 단순한 계산의 결과라고 볼 수 있다.
모든 응답이 Content-Length를 가지는 것은 아니다
문제는 HTTP 응답이 항상 Content-Length 헤더를 포함하는 것은 아니라는 점이다. 서버가 데이터를 스트림 방식으로 전송하는 경우 전체 크기를 미리 알 수 없는 상황이 매우 흔하게 발생한다. 대표적인 예를 몇 가지 들어 보면 다음과 같다.
서버가 데이터를 실시간으로 생성하는 경우
로그나 이벤트 스트림
AI 응답 스트리밍
chunked transfer encoding
이런 경우 서버는 데이터를 한 번에 모두 만들어 두고 보내는 것이 아니라, 생성되는 즉시 조금씩 전송하게 된다. 즉 데이터는 조각 단위로 계속 흘러오지만, 전체 데이터 크기는 처음부터 알 수 없는 상태가 된다. 그래서 이런 경우 브라우저는 진행률을 계산할 수 없고, 대신 단순한 로딩 스피너나 “다운로드 중” 같은 상태 표시만 보여 주게 된다.
Streams API 관점에서 보면
Streams API 관점에서 이 상황을 보면 구조가 조금 더 명확해진다. fetch()가 반환하는 Response 객체의 body는 ReadableStream<Uint8Array> 타입을 가진다. 즉 HTTP 응답은 실제로 바이트 스트림이다. 브라우저는 이 스트림을 한 번에 모두 읽는 것이 아니라, 네트워크에서 도착하는 데이터 조각을 조금씩 읽어 들인다.
예를 들어 다음과 같은 코드가 있을 수 있다.
const response = await fetch(url)
const reader = response.body!.getReader()
let received = 0
while (true) {
const { value, done } = await reader.read()
if (done) break
received += value.length
}이 코드에서 received는 지금까지 받은 바이트 수를 의미한다. 네트워크에서 데이터가 도착할 때마다 reader.read()가 새로운 바이트 청크를 반환하고, 우리는 그 크기를 계속 누적할 수 있다. 따라서 만약 Content-Length 헤더가 존재한다면 브라우저는 전체 진행률을 계산을 할 수 있게 된다.
댓글
댓글을 불러오는 중...