
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속 돌아간다. 사용자 입장에서는 둘 다 “다운로드 중”이라는 점에서는 같아 보이지만, 내부적으로는 전혀 다른 상황일 수 있다.
이 차이를 이해하려면 먼저 다운로드 진행률이 어떻게 계산되는지를 살펴볼 필요가 있다. 사실 진행률 계산 자체는 복잡한 알고리즘이 아니다. 대부분의 경우 매우 단순한 비율 계산으로 이루어진다.
다운로드 진행률의 기본 공식
다운로드 진행률은 보통 다음과 같은 방식으로 계산된다: 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 헤더가 존재한다면 브라우저는 전체 진행률을 계산을 할 수 있게 된다.
더 읽어보기
2026.03.13
Streams API 2. 상태와 백프레셔의 의미론
스트림을 처음 배울 때는 읽고 쓰는 예제가 꽤 단순해 보인다. getReader()로 읽고, getWriter()로 쓰고, pipeTo()로 연결하면 끝나는 것처럼 느껴진다. 실제로 짧은 데모는 이 정도만 알아도 돌아간다. 그러나 실무에서 스트림 코드를 망가뜨리는 원인은 거의 언제나 더…
2026.03.13
Streams API 1. 읽기와 쓰기의 출발점
자바스크립트에서 비동기를 배울 때 우리는 대개 Promise와 async, await부터 익힌다. 이 조합은 한 번의 결과를 기다리는 문제에는 매우 강력하다. 그러나 데이터가 한 번에 끝나지 않고 계속 흘러들어오는 상황에서는 이야기가 달라진다. 네트워크 응답이 길게 이어지거나, 큰 파일…
2026.03.04
JavaScript를 위한 더 나은 Streams API가 필요하다
이 포스트는 node.js의 코어 컨트리뷰트이며 Cloudflare Workers 팀 소속 개발자 James M Snell이 cloudflare 블로그에 올린 We deserve a better streams API for JavaScript 게시글을 번역한 것이다. 번역하는 과정에서…
2026.02.22
View Transition API
웹 애플리케이션에서 전환 품질은 기능 완성도와 동등한 수준으로 중요하다. 사용자가 목록에서 항목을 선택해 상세 화면으로 이동할 때, 화면이 자연스럽게 이어지면 서비스는 빠르고 안정적으로 느껴진다. 반대로 동일한 기능이라도 전환이 끊기면 체감 성능과 신뢰도는 동시에 하락한다. 전환은 부가…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
2026.03.13
Streams API 3. 바이트 스트림과 실전 파이프라인
앞선 두 글에서 우리는 Web Streams API의 표면과 의미론을 정리했다. 이제 남은 질문은 하나이다. 이 지식을 실제 어디에 써먹을 것인가. 이 질문에 답하는 순간 스트림 학습은 비로소 완성된다. 표준을 읽고 메서드를 아는 것만으로는 충분하지 않다. fetch() 응답 본문을 어…
댓글
댓글을 불러오는 중...