
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStream이 떠오를 수 있고, Go를 써 본 사람이라면 io.Reader, io.Writer가 생각날 수 있다. Rust를 써 본 사람이라면 Read, Write 트레이트를 바로 연결할 수도 있다. 이름은 조금씩 다르고, 세부 API도 다르지만, 이상할 정도로 공통된 구조가 보인다. 읽고, 처리하고, 쓰는 구조다. 데이터가 어디선가 흘러오고, 중간에서 가공되고, 다른 곳으로 흘러간다.
이쯤 되면 자연스럽게 질문이 생긴다. 왜 서로 다른 언어들이 이렇게 비슷한 구조를 가지게 되었을까. 이것은 우연이라기보다 필연에 가깝다. 스트림이라는 개념은 특정 언어가 독창적으로 발명한 추상화가 아니라, 그보다 훨씬 아래 계층에 있는 네트워크와 운영체제의 I/O 모델에서 시작되었기 때문이다. 다시 말해 언어의 Stream API는 출발점이 아니라 결과에 가깝다. 이미 존재하던 데이터 흐름의 세계를 사람이 다루기 쉽게 감싼 인터페이스가 바로 각 언어의 Stream API라고 보는 편이 더 정확하다.
네트워크는 생각보다 “메시지적”이지 않다
애플리케이션을 작성하다 보면 우리는 종종 데이터를 메시지처럼 생각한다. 서버가 “응답 하나”를 보낸다고 말하고, 클라이언트가 “요청 하나”를 보낸다고 말한다. JSON 하나를 받고, 이미지 하나를 받고, 텍스트 하나를 받는다고 이해한다. 애플리케이션 레벨에서는 이 말이 크게 틀리지 않는다. 하지만 네트워크, 특히 TCP 수준으로 내려가 보면 이야기가 달라진다. TCP는 애플리케이션에게 “완성된 메시지”를 전달하는 프로토콜이 아니라 연속된 바이트 흐름을 전달하는 프로토콜에 가깝다.
예를 들어 서버가 hello world라는 문자열을 보냈다고 해 보자. 애플리케이션 관점에서는 이것이 하나의 완결된 메시지처럼 보일 수 있다. 그러나 네트워크는 그 문자열을 “한 덩어리의 의미 있는 데이터”로 이해하지 않는다. 실제 전송 과정에서는 hel, lo w, orld처럼 임의의 경계에서 나뉘어 전달될 수도 있다. 애플리케이션이 받는 쪽에서는 이 조각들을 다시 이어 붙여야만 비로소 원래의 의미를 회복할 수 있다. 즉, 네트워크가 주는 것은 메시지가 아니라 바이트의 흐름이다. 이 점이 매우 중요하다. 왜냐하면 이 순간부터 애플리케이션은 데이터를 “조금씩 읽고, 조금씩 처리하고, 조금씩 넘기는” 방식으로 다룰 수밖에 없기 때문이다.
바로 여기서 스트림 모델이 등장한다. 스트림은 거창한 추상화처럼 보이지만, 사실 네트워크의 이 본질을 다루기 위한 매우 자연스러운 형식이다. 바이트가 끊임없이 흐르고, 우리는 그 일부를 읽고, 해석하고, 다시 다른 곳으로 보내야 한다. 이 구조를 가장 단순하게 표현하면 결국 read와 write다. 읽고, 쓰는 것. 대부분의 Stream API는 사실 이 두 동작에서 출발한다.
운영체제는 이미 오래전부터 스트림처럼 동작하고 있었다
이 모델은 네트워크에만 한정되지 않는다. 운영체제의 기본 I/O 인터페이스도 거의 같은 사고방식을 가지고 있다. POSIX 계열 시스템에서 가장 대표적인 I/O 인터페이스는 아래와 같다.
read(fd, buffer, size)
write(fd, buffer, size)여기서 중요한 것은 fd가 단순히 “파일”만을 가리키는 것이 아니라는 점이다. 운영체제 입장에서는 파일, 네트워크 소켓, 프로세스 간 파이프, 표준 입력과 출력 모두가 비슷한 형태의 I/O 대상으로 취급된다. 즉, 운영체제는 이미 매우 오래전부터 다양한 입출력 대상을 “읽고 쓸 수 있는 흐름”으로 다루고 있었다.
이 관점은 Unix 철학과도 맞닿아 있다. 파일도 읽고 쓸 수 있고, 파이프도 읽고 쓸 수 있고, 네트워크 소켓도 읽고 쓸 수 있다. 서로 다른 자원처럼 보이지만, 프로그래밍 모델 차원에서는 놀랄 만큼 비슷하다. 개발자가 다루는 것은 “무엇인가로부터 데이터를 조금씩 읽고, 무엇인가에 데이터를 조금씩 쓰는 일”이다. 결국 운영체제는 I/O를 개별적인 특수 기능의 모음으로 보기보다, 공통된 데이터 흐름 모델 위에서 바라보는 셈이다.
이런 배경을 이해하면 왜 스트림이라는 개념이 여러 언어에서 반복해서 나타나는지 감이 잡힌다. 언어는 완전히 새로운 세계를 만드는 것이 아니라, 운영체제와 네트워크가 이미 제공하고 있는 모델을 개발자가 다루기 쉽게 끌어올린다. 그러니 언어가 달라도 비슷한 API가 나오는 것은 이상한 일이 아니다. 오히려 그렇지 않은 편이 더 이상할 것이다.
언어의 Stream API는 모두 같은 문제를 각자의 방식으로 풀고 있다
프로그래밍 언어의 스트림 API는 사실 이 운영체제 모델을 조금 더 사용하기 쉽게 감싼 것이다. 데이터를 한 번에 모두 메모리에 올려 처리하는 것이 아니라, 흐름으로 들어오는 데이터를 순차적으로 읽고, 필요하면 중간에서 변환하고, 다시 다른 목적지로 보내는 문제다.
// JAVA
InputStream
OutputStream
// GO
io.Reader
io.Writer
// RUST
Read
Write이 공통점은 단순히 이름의 유사성에서 끝나지 않는다. 실제로 각 언어의 API는 거의 비슷한 철학을 공유한다. 입력과 출력을 나누고, 생산자와 소비자를 분리하고, 중간에 변환기를 둘 수 있게 하고, 큰 데이터를 작은 조각으로 나누어 처리할 수 있게 한다. 이는 곧 스트림이라는 개념이 특정 언어 문법에 의존하는 기능이 아니라, 더 아래층에 있는 현실적인 I/O 문제를 다루기 위한 일반 모델이라는 뜻이다.
이 관점에서 보면 Web Streams API도 훨씬 또렷하게 보인다. ReadableStream은 데이터가 흘러나오는 쪽을, WritableStream은 데이터가 흘러들어가는 쪽을, TransformStream은 그 사이에서 변환을 수행하는 쪽을 담당한다. 이 구조는 낯선 웹 전용 발명품이 아니라, 이미 다른 언어와 시스템에서 오랫동안 반복되어 온 구조를 웹 환경에 맞게 표준화한 결과에 가깝다.
왜 하필 “스트림”이라는 모델이 반복될까
여기서 한 걸음 더 들어가 보면, 스트림이 여러 언어에 존재하는 이유는 단지 역사적 관성 때문만은 아니다. 이 모델이 실제 문제를 푸는 데 매우 유리하기 때문이다. 현실의 데이터는 생각보다 자주 “한 번에 다루기 어려운 것”의 형태로 존재한다. 네트워크 응답은 언제 도착할지 모르고, 파일은 너무 커서 통째로 메모리에 올리기 부담스럽고, 영상과 오디오는 계속 흘러들어오며, 로그나 이벤트는 끝없이 이어질 수 있다. 이런 데이터는 배열처럼 고정된 덩어리로 생각하기보다 흐름으로 생각하는 편이 훨씬 자연스럽다.
스트림 모델은 이런 현실을 그대로 반영한다. 전체가 완성되기 전에 일부를 먼저 처리할 수 있고, 메모리를 과도하게 쓰지 않아도 되며, 여러 단계를 파이프라인으로 연결하기 쉽다. 앞선 글에서 살펴본 백프레셔, 바이트 스트림, BYOB 같은 개념도 모두 이 큰 그림 안에서 이해할 수 있다. 결국 스트림은 단순한 편의 API가 아니라, 연속적으로 도착하는 데이터를 효율적으로 다루기 위해 여러 시스템이 수렴한 공통 설계라고 볼 수 있다.
Stream API는 언어 기능이 아니라 I/O 모델의 번역본에 가깝다
이제 방향을 조금 바꿔 생각해 볼 수 있다. 우리는 종종 ReadableStream이나 WritableStream을 JavaScript의 기능이라고 말한다. 물론 실용적으로는 맞는 표현이다. 하지만 개념적으로는 이것이 조금 부족하다. 더 정확히 말하면, Stream API는 네트워크와 운영체제가 제공하는 I/O 모델을 각 언어가 자기 식으로 번역한 결과물에 가깝다.
이렇게 이해하면 몇 가지가 동시에 정리된다. 왜 여러 언어가 비슷한 구조를 가지는지 설명된다. 왜 스트림을 배우다 보면 파일, 네트워크, 파이프, 표준 입출력 같은 이야기가 자꾸 함께 등장하는지도 설명된다. 그리고 왜 스트림이 단순한 문법 설탕이 아니라, 성능과 메모리, 흐름 제어, 경계 처리 같은 실제 시스템 문제와 깊게 연결되는지도 설명된다.
특히 Web Streams API를 공부하는 입장에서는 이 관점이 중요하다. pipeTo, pipeThrough, tee, TextDecoderStream, BYOB 같은 개별 API를 따로따로 외우는 것보다, 이 모든 것이 결국 “흐르는 데이터를 어떻게 읽고, 가공하고, 전달할 것인가”라는 오래된 문제의 웹 표준 버전이라고 이해하는 편이 훨씬 오래 남는다. 표면 API는 시대와 환경에 따라 달라질 수 있지만, 그 아래의 문제는 꽤 오래 같은 모습으로 남아 있기 때문이다.
더 읽어보기
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.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 3. 바이트 스트림과 실전 파이프라인
앞선 두 글에서 우리는 Web Streams API의 표면과 의미론을 정리했다. 이제 남은 질문은 하나이다. 이 지식을 실제 어디에 써먹을 것인가. 이 질문에 답하는 순간 스트림 학습은 비로소 완성된다. 표준을 읽고 메서드를 아는 것만으로는 충분하지 않다. fetch() 응답 본문을 어…
댓글
댓글을 불러오는 중...