Using readable streams

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ฐœ๋ฐœ์ž๋กœ์„œ, ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ ์œผ๋กœ ๋„คํŠธ์›Œํฌ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ Chunk๋‹จ์œ„๋กœ ์ฝ๊ณ  ๋‹ค๋ฃจ๋Š” ๊ฒƒ์€ ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค! ๊ทธ๋Ÿฌ๋‚˜ ์–ด๋–ป๊ฒŒ ์ŠคํŠธ๋ฆผ API์˜ Readable stream์„ ์ž˜ ์‚ฌ์šฉํ• ์ˆ˜ ์žˆ์„๊นŒ์š”. ์ด๋ฒˆ ๋‚ด์šฉ์€ ๊ทธ๊ฒƒ์„ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Note: This article assumes that you understand the use cases of readable streams, and are aware of the high-level concepts. If not, we suggest that you first read the Streams concepts and usage overview and dedicated Streams API concepts article, then come back.

Note: If you are looking for information on writable streams try Using writable streams instead.

Browser support

ํŒŒ์ด์–ดํญ์Šค 65+ ์™€ ํฌ๋กฌ 42+ ์—์„œ Fetch Body ๊ฐ์ฒด๋ฅผ ์ŠคํŠธ๋ฆผ์œผ๋กœ์„œ ์‚ฌ์šฉ ํ• ์ˆ˜ ์žˆ๊ณ , custom readable ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ค์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ Pipe chains์˜ ๊ฒฝ์šฐ ์˜ค์ง ํฌ๋กฌ์—์„œ๋งŒ ์ง€์›ํ•˜๊ณ  ์žˆ๊ณ  ๊ทธ ๊ธฐ๋Šฅ์€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Finding some examples

์ด๋ฒˆ ๋‚ด์šฉ๊ณผ ๊ด€๋ จํ•œ ๋งŽ์€ ์˜ˆ์ œ๋ฅผ dom-examples/streams ์—์„œ ์‚ดํŽด๋ณผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ณณ์—์„œ ๋ชจ๋“  ์†Œ์Šค๋ฅผ ํ™•์ธํ• ์ˆ˜ ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์˜ˆ์ œ ์‚ฌ์ดํŠธ ๋งํฌ๋„ ํ™•์ธํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Consuming a fetch as a stream

Fetch API ๋Š” ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ๋ฆฌ์†Œ์Šค๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” XHR์˜ ํ˜„๋Œ€์ ์ธ ๋Œ€์•ˆ์ฑ… ์ž…๋‹ˆ๋‹ค. Fetch API์˜ ์ˆ˜๋งŽ์€ ์ด์  ๊ฐ€์šด๋ฐ ๊ฐ€์žฅ ํ›Œ๋ฅญํ•œ์ ์€ ์ตœ๊ทผ ๋ธŒ๋ผ์šฐ์ €๋“ค์ด Fetch Response๋ฅผ Readable ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ด์šฉํ• ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๊ฒƒ ์ž…๋‹ˆ๋‹ค.

Body ๋ฏน์Šค์ธ์€ body ์†์„ฑ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด body ์†์„ฑ์€ body์˜ ๋‚ด์šฉ์„ Readable ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋…ธ์ถœ์‹œํ‚ค๋Š” ๊ฐ„๋‹จํ•œ getter ์ž…๋‹ˆ๋‹ค. ์ด Body ๋ฏน์Šค์ธ์€ Request์™€ Response ์ธํ„ฐํŽ˜์ด์Šค๋กœ๋ถ€ํ„ฐ ๊ตฌํ˜„ ๋˜๋ฉฐ, ๋”ฐ๋ผ์„œ ๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ ์‚ฌ์šฉ ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ Response body์˜ ์ŠคํŠธ๋ฆผ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์กฐ๊ธˆ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ์˜ Simple stream pump์˜ˆ์‹œ์—์„œ ๋ณด์—ฌ์ฃผ๋“ฏ(see it live also), Readable ์ŠคํŠธ๋ฆผ์„ ๋…ธ์ถœ์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹จ์ง€ Response์˜ body ์†์„ฑ์— ์ ‘๊ทผํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€๋ฅผ Fetch ํ•จ
fetch('./tortoise.png')
// body ๋ฅผ ReadableStream์œผ๋กœ ๊ฐ€๊ณตํ•จ
.then(response => response.body)

์ด๊ฒƒ์€ ์šฐ๋ฆฌ์—๊ฒŒ ReadableStream  ๊ฐ์ฒด๋ฅผ ์ œ๊ณตํ•ด ์ค๋‹ˆ๋‹ค.

Attaching a reader

์ด์ œ ์šฐ๋ฆฌ๋Š” ์ŠคํŠธ๋ฆผํ™”๋œ body๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด ์ŠคํŠธ๋ฆผ์„ ์ฝ๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฆฌ๋”๊ธฐ๋ฅผ ๋ถ™์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์€ ReadableStream.getReader() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

// ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€๋ฅผ Fetch ํ•จ
fetch('./tortoise.png')
// body ๋ฅผ ReadableStream์œผ๋กœ ๊ฐ€๊ณตํ•จ
.then(response => response.body)
.then(body => {
  const reader = body.getReader();

์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋ฆฌ๋”๊ธฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์ด ๋ฆฌ๋”๊ธฐ๋ฅผ ์ŠคํŠธ๋ฆผ์— ๊ณ ์ •(locks) ์‹œํ‚ต๋‹ˆ๋‹ค. ReadableStreamDefaultReader.releaseLock() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋“ฑ ์ด ๊ณ ์ •(locks)์„ ํ’€๊ธฐ์ „ ๊นŒ์ง€๋Š”(released), ๊ทธ ์–ด๋–ค ๋‹ค๋ฅธ ๋ฆฌ๋”๊ธฐ๋“ค๋„ ์ด ์ŠคํŠธ๋ฆผ์„ ์ฝ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. 

๋˜ํ•œ response.body ๋Š” ๋™๊ธฐ์ด๋ฏ€๋กœ ๊ตณ์ด ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ, ์œ„์˜ ์˜ˆ์‹œ๋ฅผ ํ•œ๋ฒˆ์˜ ์Šคํ…์œผ๋กœ ์ค„์ผ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์— ์ฃผ๋ชฉํ•ด ์ฃผ์‹ญ์‹œ์š”

// ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€๋ฅผ Fetch ํ•จ
  fetch('./tortoise.png')
  // body ๋ฅผ ReadableStream์œผ๋กœ ๊ฐ€๊ณตํ•จ
  .then(response => {
    const reader = response.body.getReader();

Reading the stream

์ด์ œ ์šฐ๋ฆฌ๋Š” ์šฐ๋ฆฌ์˜ ๋ฆฌ๋”๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋ฆฌ๋”๊ธฐ์˜ ReadableStreamDefaultReader.read() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ถ€ํ„ฐ data chunk๋“ค์„ ์ฝ์„์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ •ํ™•ํ•˜๊ฒŒ๋Š” ์ด ๋ฉ”์„œ๋“œ๋Š” ๊ฐ ์ŠคํŠธ๋ฆผ์œผ๋กœ๋ถ€ํ„ฐ ํ•˜๋‚˜์˜  data chunk๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฌํ•œ data chunk๋Š” ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š”๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์šฐ๋ฆฌ์˜ Simple stream pump example์—์„œ๋Š” ReadableStreamDefaultReader.read() ์‚ฌ์šฉํ•˜์—ฌ Data Chunk๋ฅผ ์ƒˆ๋กœ์šด ์ปค์Šคํ…€ ReadableStream์— ์ง‘์–ด ๋„ฃ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋งŒ์•ฝ ์ฝ์„์ˆ˜ ์žˆ๋Š” ๋‹ค์Œ Data Chunk๊ฐ€ ์žˆ๋‹ค๋ฉด, ReadableStreamDefaultReader.read() ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ Data Chunk๋ฅผ ์ปค์Šคํ…€ ReadableStream์— ์ง‘์–ด ๋„ฃ์Šต๋‹ˆ๋‹ค. ๋”์ด์ƒ ์ฝ์„์ˆ˜ ์žˆ๋Š” Data Chunk๊ฐ€ ์—†๋‹ค๋ฉด, ๊ทธ ์ปค์Šคํ…€ ReadableStream(์šฐ๋ฆฌ๋Š” ์ด ์ƒˆ๋กœ์šด Readable ์ŠคํŠธ๋ฆผ์— ๋Œ€ํ•ด ๋‹ค์Œ ์„น์…˜์—์„œ ๋‹ค ์ž์„ธํžˆ ์‚ดํŽด ๋ณผ๊ฒƒ ์ž…๋‹ˆ๋‹ค.)์„ ํ†ตํ•ด ์ƒˆ๋กœ์šด Response ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋‹ค์Œ ์ด Response ๊ฐ์ฒด๋ฅผ Blob ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ   ์ด Blob ์œผ๋กœ ๋ถ€ํ„ฐ URL.createObjectURL() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ URL ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ ์ด URL๊ฐ์ฒด๋ฅผ {htmlelement("img")}} ์— ์ ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์คŒ์œผ๋กœ์„œ fetch๋œ ์˜ค๋ฆฌ์ง€๋„ ์ด๋ฏธ์ง€๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๋ณต์‚ฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  return new ReadableStream({
    start(controller) {
      return pump();
      function pump() {
        // ์ŠคํŠธ๋ฆผ์˜ ๋‹ค์Œ Chunk์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” psomise๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.
        return reader.read().then(({ done, value }) => {
          // ๋”์ด์ƒ ์ฝ์„ ๋ฐ์ดํ„ฐ ์กฐ๊ฐ์ด ์—†์„๋•Œ ์ŠคํŠธ๋ฆผ์„ ๋‹ซ๋Š”๋‹ค
          if (done) {
              controller.close();
              return;
          }
          // ๋ฐ์ดํ„ฐ ์กฐ๊ฐ์„ ์ƒˆ๋กœ์šด ์ŠคํŠธ๋ฆผ(์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ)์— ๋„ฃ๋Š”๋‹ค.
          controller.enqueue(value);
          return pump();
        });
      }
    }
  })
})
.then(stream => new Response(stream))
.then(response => response.blob())
.then(blob => URL.createObjectURL(blob))
.then(url => console.log(image.src = url))
.catch(err => console.error(err));

์–ด๋–ป๊ฒŒ read() ๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€ ์ž์„ธํžˆ ๋“ค์—ฌ๋‹ค ๋ด…์‹œ๋‹ค. ์œ„ ์˜ˆ์ œ์˜ pump() ํ•จ์ˆ˜๋Š” ์ œ์ผ๋จผ์ € read() ๋ฅผ ์‹คํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด read() ๋Š” ์ŠคํŠธ๋ฆผ์œผ๋กœ๋ถ€ํ„ฐ ์ฝ์–ด ๋“ค์ธ ๋‚ด์šฉ์˜ ๊ฒฐ๊ณผ๋ฅผ { done, value } ์˜ ํ˜•์‹์œผ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” pomise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. 

return reader.read().then(({ done, value }) => {

์ŠคํŠธ๋ฆผ์œผ๋กœ๋ถ€ํ„ฐ ์ฝ์–ด ๋“ค์ธ ๋‚ด์šฉ์€ ์•„๋ž˜ 3๊ฐ€์ง€ ํƒ€์ž…์ด ์žˆ์Šต๋‹ˆ๋‹ค. 

  • chunk๋ฅผ ์•„์ง ์ฝ์„์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ์— ํ”„๋กœ๋ฏธ์Šค๋Š” { value: theChunk, done: false } ๊ฐ์ฒด์™€ ํ•จ๊ป˜ fulfill ๋ฉ๋‹ˆ๋‹ค. 
  • ์ŠคํŠธ๋ฆผ์ด ๋‹ซํžŒ ๊ฒฝ์šฐ์— ํ”„๋กœ๋ฏธ์Šค๋Š” value: undefined, done: true } ๊ฐ์ฒด์™€ ํ•จ๊ป˜ fulfill ๋ฉ๋‹ˆ๋‹ค.
  • ์ŠคํŠธ๋ฆผ์—์„œ ์• ๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ promise๋Š” ๊ด€๋ จ ์• ๋Ÿฌ์™€ ํ•จ๊ป˜ reject ๋ฉ๋‹ˆ๋‹ค. 

๋‹ค์Œ์œผ๋กœ ์šฐ๋ฆฌ๋Š” done ์ด true ์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธํ•ด ๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ done ์ด true ๋ผ๋ฉด ๋”์ด์ƒ ์ฝ์–ด๋“ค์ผ chunk๊ฐ€ ์—†๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” ํ•จ์ˆ˜ ๋ฐ–์œผ๋กœ ๋น ์ ธ ๋‚˜๊ฐ€์•ผ ํ•˜๊ณ  ReadableStreamDefaultController.close() ๋ฅผ ํ†ตํ•˜์—ฌ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์„ ๋‹ซ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. 

if (done) {
  controller.close();
  return;
}

Note: ์—ฌ๊ธฐ์„œ ์‚ฌ์šฉํ•œ close() ๋Š” ์ƒˆ๋กœ๋งŒ๋“  ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์˜ ์ผ๋ถ€์ด๋ฉฐ ์˜ค๋ฆฌ์ง€๋„ ์ŠคํŠธ๋ฆผ์˜ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ๋Œ€ํ•ด์„œ๋Š” ๋‹ค์Œ์„น์…˜์—์„œ ๋” ์ž์„ธํžˆ ์‚ดํŽด ๋ณผ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. 

๋งŒ์•ฝ done ์ด true ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด, ์šฐ์„  ์ฝ์–ด ๋“œ๋ฆฐ Chunk๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  (value ์†์„ฑ), pump() ํ•จ์ˆ˜๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ๋‹ค์‹œ ํ˜ธ์ถœ ํ•จ์œผ๋กœ์„œ ๋‹ค์Œ chunk๋ฅผ ์ฝ์–ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. 

// ๋‹ค์Œ data chunk๋ฅผ ์ƒˆ๋กœ์šด readable ์ŠคํŠธ๋ฆผ์— ์ง‘์–ด ๋„ฃ์Œ
controller.enqueue(value);
return pump();

๋‹ค์Œ์€ ์ŠคํŠธ๋ฆผ ๋ฆฌ๋”๊ธฐ๋ฅผ ์‚ฌ์šฉํ• ๋•Œ์˜ ๊ธฐ๋ณธ์ ์ธ ํŒจํ„ด ์ž…๋‹ˆ๋‹ค.

  1. ์ŠคํŠธ๋ฆผ์„ ์ฝ์Œ์œผ๋กœ์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ๋งŒ์•ฝ ์ฝ์„์ˆ˜ ์žˆ๋Š” ์ŠคํŠธ๋ฆผ์ด ๋”์ด์ƒ ์—†๋‹ค๋ฉด, ํ•จ์ˆ˜๋ฅผ ๋ฆฌํ„ด ์‹œํ‚ต๋‹ˆ๋‹ค.
  3. ๋งŒ์•ฝ ์ฝ์„์ˆ˜ ์žˆ๋Š” ์ŠคํŠธ๋ฆผ์ด ์•„์ง ๋‚จ์•„ ์žˆ๋‹ค๋ฉด, ์šฐ์„  ์ฝ์–ด ๋“œ๋ฆฐ chunk๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋‹ค์Œ chunk๋ฅผ ์ฝ์–ด ๋“œ๋ฆฌ๊ธฐ ์œ„ํ•ด ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. 
  4. ๋”์ด์ƒ ์ฝ์„์ˆ˜ ์žˆ๋Š” ์ŠคํŠธ๋ฆผ์ด ์—†์„๋•Œ๊นŒ์ง€ ํ•จ์ˆ˜๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์‹คํ–‰ํ•˜๊ณ , ์ตœ์ข…์ ์œผ๋กœ ์ฝ์„์ˆ˜ ์žˆ๋Š” ์ŠคํŠธ๋ฆผ์ด ์—†์„๊ฒฝ์šฐ 2๋ฒˆ Step์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. 

Creating your own custom readable stream

๋‘๋ฒˆ์งธ ํŒŒํŠธ์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ Simple stream pump example ์˜ˆ์ œ์—์„œ ์šฐ๋ฆฌ๋Š” fetch body๋กœ ๋ถ€ํ„ฐ ์ฝ์–ด๋“œ๋ฆฐ ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ data๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์ž์ฒด์ ์œผ๋กœ ๋งŒ๋“  ์ปค์Šคํ…€ readable ์ŠคํŠธ๋ฆผ์— ๋‹ค์‹œ ์˜ฎ๊ฒจ ์‹ฌ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒ ์ŠคํŠธ๋ฆผ์„์ž์ฒด์ ์œผ๋กœ ๋งŒ๋“ค์ˆ˜ ์žˆ์„๊นŒ์š”? ์šฐ๋ฆฌ๋Š” ReadableStream()์ƒ์„ฑ์ž ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ปค์Šคํ…€ readable ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

The ReadableStream() constructor

Fetch์™€ ๊ฐ™์ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ŠคํŠธ๋ฆผ์„ ์ œ๊ณตํ• ๋•Œ ๊ทธ๊ฒƒ์„ ์ฝ์–ด ๋“ค์ด๋Š” ์ผ์€ ์‰ฌ์šด ์ผ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋•Œ๋•Œ๋กœ ์ƒˆ๋กœ์šด ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ค๊ณ  ์ด๊ฒƒ์„ data chunk๋“ค๋กœ ์ฑ„์›Œ๋„ฃ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ReadableStream()์„ ์•„๋ž˜์˜ ๊ตฌ๋ฌธ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•จ์œผ๋กœ์„œ ์ด๊ฒƒ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ๋ฌธ์ด ์ฒ˜์Œ์—๋Š” ๋‹ค์†Œ ๋ณต์žกํ•ด ๋ณด์ผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ์‹ค์ œ๋กœ๋Š” ๊ทธ๋ ‡๊ฒŒ ๋ณต์žกํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 

๊ธฐ๋ณธ์ ์ธ ํ•ต์‹ฌ ๊ตฌ๋ฌธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const stream = new ReadableStream({
  start(controller) {

  },
  pull(controller) {

  },
  cancel() {

  },
  type,
  autoAllocateChunkSize
}, {
  highWaterMark,
  size()
});

์ƒ์„ฑ์ž ํ•จ์ˆ˜๋Š” ๋‘๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ์ธ์ž๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ ์ธ์ž๋Š” ํ•„์ˆ˜ ๊ฐ’์ด๋ฉฐ, ์ด๊ฒƒ์€ ์šฐ๋ฆฌ๊ฐ€ ์ฝ์–ด ๋“ค์ผ ๊ธฐ๋ณธ ์†Œ์Šค์˜ ๋ชจ๋ธ์„ JavasScrip ์ ์œผ๋กœ ์ƒ์„ฑ ํ•ฉ๋‹ˆ๋‹ค.  ๋‘๋ฒˆ์งธ ์ธ์ž๋Š” ์˜ต์…˜ ๊ฐ’์ด๋ฉฐ, ์ด๊ฒƒ์€ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ์‚ฌ์šฉํ•  ์ปค์Šคํ…€ queuing ์ „๋žต์„ ์ˆ˜๋ฆฝํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋‘๋ฒˆ์งธ ์ธ์ž์˜ ๊ฒฝ์šฐ ๋งค์šฐ ๋“œ๋ฌผ๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฐ’์ด๋ฏ€๋กœ ์ง€๊ธˆ์€ ์ฒซ๋ฒˆ์งธ ์ธ์ž์— ์ง‘์ค‘ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ฒซ๋ฒˆ์งธ ์ธ์ž์ธ ๊ฐ์ฒด๋Š” 5๊ฐœ์˜ ๋งด๋ฒ„๋ฅผ ๊ฐ€์งˆ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ œ์ผ ์ฒซ๋ฒˆ์งธ ๋งด๋ฒ„๋งŒ ํ•„์ˆ˜ ์ž…๋‹ˆ๋‹ค.

  1. start(controller) โ€” ReadableStream ์ด ์ƒ์„ฑ๋˜์ž ๋งˆ์ž ๋”ฑ ํ•œ๋ฒˆ๋งŒ ํ˜ธ์ถœ ๋˜๋Š” ๋ฉ”์„œ๋“œ ์ž…๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ์—๋Š” ์ŠคํŠธ๋ฆผ์„ ๊ธฐ๋Šฅ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋ฐ์ดํ„ฐ ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•œ๋‹ค๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด ์†Œ์Šค์— ์ ‘๊ทผํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ๋“ฑ์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  2. pull(controller) โ€” ์ด ๋ฉ”์„œ๋“œ๋Š” ์ŠคํŠธ๋ฆผ ๋‚ด๋ถ€์˜ queue๊ฐ€ ๊ฐ€๋“ ์ฐฐ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœ ๋ฉ๋‹ˆ๋‹ค. ๋” ๋งŽ์€ ์ฒญํฌ๊ฐ€ ํ์— ๋“ค์–ด๊ฐˆ ๋•Œ ์ŠคํŠธ๋ฆผ์„ ์ œ์–ดํ•˜๋Š” โ€‹โ€‹๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. cancel() โ€” ์ด ๋ฉ”์„œ๋“œ๋Š” ์ŠคํŠธ๋ฆผ์ด ์บ”์Šฌ๋ ๋•Œ ํ˜ธ์ถœ ๋ฉ๋‹ˆ๋‹ค (์˜ˆ๋ฅผ ๋“ค์–ด {domxref("ReadableStream.cancel()")}} ์ด ํ˜ธ์ถœ ๋˜์—ˆ์„๋•Œ). ๋ฉ”์„œ๋“œ์˜ ๋‚ด์šฉ์€ ์ŠคํŠธ๋ฆผ ์†Œ์Šค์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ํ•ด์ œํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ์ˆ˜ํ–‰ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
  4. type and autoAllocateChunkSize โ€”  ์ŠคํŠธ๋ฆผ์ด ๋ฐ”์ดํŠธ ์ŠคํŠธ๋ฆผ์ž„์„ ๋‚˜ํƒ€ ๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋ฐ”์ดํŠธ ์ŠคํŠธ๋ฆผ์€ ๋ชฉ์ ๊ณผ ์‚ฌ์šฉ ์‚ฌ๋ก€๊ฐ€ ์ผ๋ฐ˜ (๊ธฐ๋ณธ) ์ŠคํŠธ๋ฆผ๊ณผ ์•ฝ๊ฐ„ ๋‹ค๋ฅด๋ฏ€๋กœ ํ–ฅํ›„ ์ž์Šต์„œ์—์„œ ๋ณ„๋„๋กœ ๋‹ค๋ฃฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ ์•„์ง ์–ด๋Š ๊ณณ์—์„œ๋„ ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

simple example code ๋ฅผ ๋‹ค์‹œํ•œ๋ฒˆ ์‚ดํŽด๋ณด๋ฉด, ReadableStream() ์ƒ์„ฑ์ž๊ฐ€ start() ๋ฉ”์„œ๋“œ ๋‹จ ํ•˜๋‚˜๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•„์ฑŒ์ˆ˜ ์žˆ์„ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ์ด start() ๋ฉ”์„œ๋“œ fetch๋œ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด ๋“ค์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  return new ReadableStream({
    start(controller) {
      return pump();
      function pump() {
        return reader.read().then(({ done, value }) => {
          // ๋”์ด์ƒ ์ฝ์„์ˆ˜ ์žˆ๋Š” data๊ฐ€ ์—†๋‹ค๋ฉด ์ŠคํŠธ๋ฆผ์„ ๋‹ซ๋Š”๋‹ค
          if (done) {
            controller.close();
            return;
          }
          // ๋ฐ์ดํ„ฐ ์กฐ๊ฐ์„ ์ƒˆ๋กœ์šด ์ŠคํŠธ๋ฆผ(์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ)์— ๋„ฃ๋Š”๋‹ค.
          controller.enqueue(value);
          return pump();
        });
      }
    }
  })
})

ReadableStream controllers

ReadableStrem() ์ƒ์„ฑ์ž์— ์ธ์ž๋กœ ์ „๋‹ฌ๋œ ๊ฐ์ฒด์•ˆ์˜ start() ์™€ pull() ๋ฉ”์„œ๋“œ์— controller๋ผ๋Š” ์ธ์ž๊ฐ€ ์ „๋‹ฌ๋˜๋Š” ๊ฒƒ์„ ๋ณผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ReadableStreamDefaultController ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค์ด๋ฉฐ ์šฐ๋ฆฌ์˜ ์ŠคํŠธ๋ฆผ์„ ์ œ์–ดํ•˜๋Š”๋ฐ ์‚ฌ์šฉ ๋ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ์˜ ์˜ˆ์ œ์—์„œ, ์šฐ๋ฆฌ๋Š” fetch๋œ body๋กœ๋ถ€ํ„ฐ chunk์˜ ๊ฐ’์„ ์ฝ์€ ๋’ค ๊ทธ ๊ฐ’์„ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ์ง‘์–ด ๋„ฃ๊ธฐ ์œ„ํ•ด Controller์˜ enqueue() ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, fetch๋œ body๋ฅผ ์ฝ์–ด ๋“ค์ด๋Š” ๊ฒƒ์ด ๋๋‚˜๋ฉด ์ปจํŠธ๋กค๋Ÿฌ์˜ close()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ์ด๋ฏธ ์‚ฝ์ž…๋œ chunk๋“ค์€ ์—ฌ์ „ํžˆ ์ฝ์„์ˆ˜ ์žˆ์ง€๋งŒ ์ƒˆ๋กœ์šด chunk๋Š” ์ง‘์–ด ๋„ฃ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ฝ๋Š” ๊ฒƒ์ด ๋ชจ๋‘ ๊ธ‘๋‚˜๋ฉด ์ŠคํŠธ๋ฆผ์€ ๋‹ซํž™๋‹ˆ๋‹ค. 

Reading from custom streams

์šฐ๋ฆฌ์˜ Simple stream pump example ์—์„œ, ์šฐ๋ฆฌ๋Š” {domxref("Response.Response", "Response")}} ์ƒ์„ฑ์ž ํ•จ์ˆ˜์— ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์ปค์Šคํ…€ readable ์ŠคํŠธ๋ฆผ์„ ์ธ์ž๋กœ ์ „๋‹ฌํ•˜์˜€์œผ๋ฉฐ ๊ทธ๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ response ์ธ์Šคํ„ด์Šค๋ฅผ blob() ์œผ๋กœ ์‚ฌ์šฉ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

.then(stream => new Response(stream))
.then(response => response.blob())
.then(blob => URL.createObjectURL(blob))
.then(url => console.log(image.src = url))
.catch(err => console.error(err));

๊ทธ๋Ÿฌ๋‚˜ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์€ ์—ฌ์ „ํžˆ ReadableStream ์ธ์Šคํ„ด์Šค์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์šฐ๋ฆฌ๋Š” ์—ฌ์ „ํžˆ ๋ฆฌ๋”๊ธฐ๋ฅผ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ๋ถ™์ผ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Simple random stream demo (see it live also) ๋ฅผ ์‚ดํŽด ๋ณด์‹ญ์‹œ์š”. ์ด ์˜ˆ์ œ์—์„œ๋Š” ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์„ ์ƒ์„ฑํ•œํ›„, ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์ƒ์„ฑ๋œ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ์ง‘์–ด ๋„ฃ์Šต๋‹ˆ๋‹ค. ๊ทธ ํ›„ ๋ฌธ์ž์—ด ์ƒ์„ฑ ์ค‘์ง€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„๋•Œ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์— ์ง‘์–ด ๋„ฃ์—ˆ๋˜ ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ฝ์–ด ์˜ต๋‹ˆ๋‹ค.

Note: FetchEvent.respondWith() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ŠคํŠธ๋ฆผ์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ŠคํŠธ๋ฆผ์— ์‚ฝ์ž…๋œ ๋‚ด์šฉ์ด ๋งŒ๋“œ์‹œ Uint8Array ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (TextEncoder ๋“ฑ์„ ์‚ฌ์šฉ)

 Simple random stream demo (see it live also) ์—์„œ ์ปค์Šคํ…€ ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ์ž ํ•จ์ˆ˜๋Š” start() ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด ๋ฉ”์„œ๋“œ๋Š” WindowTimers.setInterval() ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งค์ดˆ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์ƒ์„ฑํ•˜๊ณ  ์ด ๋ฌธ์ž์—ด์„ ReadableStreamDefaultController.enqueue() ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ŠคํŠธ๋ฆผ์•ˆ์— ๋„ฃ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ฌธ์ž์—ด ์ƒ์„ฑ ์ค‘์ง€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค๋ฉด ์ด interval ์ด ์ทจ์†Œ๋จ๊ณผ ๋™์‹œ์— readStream() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ถ€ํ„ฐ ์ŠคํŠธ๋ฆผ์— ๋„ฃ์€ ๋ฌธ์ž์—ด ์ „๋ถ€๋ฅผ ์ฝ์–ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์ด๋•Œ ์ŠคํŠธ๋ฆผ์— chunk data(์—ฌ๊ธฐ์„œ๋Š” ๋žœ๋ค ๋ฌธ์ž์—ด)๋ฅผ ๋„ฃ๋Š” ๊ฒƒ๋„ ์ค‘๋‹จ ํ–ˆ๊ธฐ๋•Œ๋ฌธ์— ์ŠคํŠธ๋ฆผ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค. 

const stream = new ReadableStream({
  start(controller) {
    interval = setInterval(() => {
      let string = randomChars();
      // Add the string to the stream
      controller.enqueue(string);
      // show it on the screen
      let listItem = document.createElement('li');
      listItem.textContent = string;
      list1.appendChild(listItem);
    }, 1000);
    button.addEventListener('click', function() {
      clearInterval(interval);
      readStream();
      controller.close();
    })
  },
  pull(controller) {
    // We don't really need a pull in this example
  },
  cancel() {
    // This is called if the reader cancels,
    // so we should stop generating strings
    clearInterval(interval);
  }
});

์ŠคํŠธ๋ฆผ์— ๋„ฃ์€ ๋ฌธ์ž์—ด ์ „๋ถ€๋ฅผ ์ฝ๊ณ  ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด ๋งŒ๋“  ์ปค์Šคํ…€ ํ•จ์ˆ˜์ธ readStream() ํ•จ์ˆ˜๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ์šฐ์„  ReadableStream.getReader()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ๋”๊ธฐ๋ฅผ ์ŠคํŠธ๋ฆผ์— ๊ณ ์ •์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ณผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ํ›„ ์•ž์„œ ์‚ดํŽด๋ดค๋˜ ํŒจํ„ด๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ  read() ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ chunk๋ฅผ ์ฝ์–ด ๋“ค์ด๊ณ   done ์ด true ์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ true ์ด๋ฉด  readStream() ํ•จ์ˆ˜์˜ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋๋‚ด๋ฒ„๋ฆฌ๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์ฝ์–ด๋“œ๋ฆฐ chunk๋ฅผ ํ›„์†์ฒ˜๋ฆฌํ•œ ํ›„  read() ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

function readStream() {
  const reader = stream.getReader();
  let charsReceived = 0;

  // read() returns a promise that resolves
  // when a value has been received
  reader.read().then(function processText({ done, value }) {
    // Result objects contain two properties:
    // done  - true if the stream has already given you all its data.
    // value - some data. Always undefined when done is true.
    if (done) {
      console.log("Stream complete");
      para.textContent = result;
      return;
    }

    charsReceived += value.length;
    const chunk = value;
    let listItem = document.createElement('li');
    listItem.textContent = 'Read ' + charsReceived + ' characters so far. Current chunk = ' + chunk;
    list2.appendChild(listItem);

    result += chunk;

    // Read some more, and call this function again
    return reader.read().then(processText);
  });
}

Closing and cancelling streams

์šฐ๋ฆฌ๋Š” ์ด๋ฏธ ์•ž์„œ ์ŠคํŠธ๋ฆผ์„ ๋‹ซ๋Š” ๋ฉ”์„œ๋“œ์ธ ReadableStreamDefaultController.close() ๋ฅผ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ์–ธ๊ธ‰ํ–ˆ๋‹ค ์‹œํ”ผ, ์ŠคํŠธ๋ฆผ์ด ๋‹ซํ˜”๋‹ค ํ•˜๋”๋ผ๊ณ  ์ด๋ฏธ ๋“ค์–ด๊ฐ€ ์žˆ๋Š” chunk๋Š” ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ŠคํŠธ๋ฆผ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ œ๊ฑฐํ•˜๊ณ  ์‚ฝ์ž…๋œ ๋ชจ๋“  chunk๋ฅผ ๋‚ ๋ฆฌ๊ณ  ์‹ถ๋‹ค๋ฉด, ReadableStream.cancel() ๋˜๋Š” ReadableStreamDefaultReader.cancel() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Teeing a stream

๋•Œ๋กœ๋Š” ํ•˜๋‚˜์˜ ์ŠคํŠธ๋ฆผ์„ ๋™์‹œ ๋‘๋ฒˆ ์ฝ์–ด๋“ค์—ฌ์•ผ ํ•  ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ReadableStream.tee() ๋ฉ”์„œ๋“œ๊ฐ€ ์ด๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ReadableStream.tee() ๋ฉ”์„œ๋“œ๋Š” ๋‘๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ์นดํ”ผ๋œ ์ŠคํŠธ๋ฆผ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ฐฐ์—ด์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ์นดํ”ผ๋œ ๋‘๊ฐœ์˜ ์ŠคํŠธ๋ฆผ์€ ๋‘๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ๋ฆฌ๋”๊ธฐ๋กœ ๊ฐ๊ฐ ์ฝ์–ด ๋“ค์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ๋Š” ์•„๋งˆ  ServiceWorker ์•ˆ์—์„œ ํ•„์š”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ fetch๋œ response๋ฅผ ๋ธŒ๋ผ์šฐ์ €์—๋„ ์ „๋‹ฌํ•˜๊ณ  ์„œ๋น„์Šค ์›Œ์ปค ์บ์‹œ์—๋„ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋‹ค๋ฉด ํ•˜๋‚˜์˜ ์ŠคํŠธ๋ฆผ์— ๋Œ€ํ•ด ๋‘๊ฐœ์˜ ์นดํ”ผ๋ณธ์ด ํ•„์š” ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด response body (Readablestream)๋Š” ๋‹จ ํ•œ๋ฒˆ๋งŒ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๊ณ  ํ•˜๋‚˜์˜ Readablestream์€ ํ•˜๋‚˜์˜ ๋ฆฌ๋”๊ธฐ๋งŒ ๋ถ™์„์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. 

์œ„ ๋‚ด์šฉ์— ๋Œ€ํ•œ ์˜ˆ์ œ๋ฅผ Simple tee example (see it live also)์—์„œ ์‚ดํŽด ๋ณผ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋Š” ๋žœ๋ค ๋ฌธ์ž์—ด ์ƒ์„ฑ ๋ฒ„ํŠผ์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์—†๋‹ค๋Š” ์ ๊ณผ, ์ด ์˜ˆ์ œ์—์„œ์˜ ์ŠคํŠธ๋ฆผ์€ teed๋˜์–ด ๋‘๊ฐœ์˜ ์ŠคํŠธ๋ฆผ์ด ๋‘๊ฐœ์˜ ๋ฆฌ๋”๊ธฐ๋กœ ์ฝ์–ด์ง„๋‹ค๋Š” ์ ๋งŒ ์ œ์™ธํ•˜๋ฉด ์•ž์„œ ์‚ดํŽด๋ณธ Simple random stream ์˜ˆ์ œ์™€ ๋งค์šฐ ์œ ์‚ฌํ•˜๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

function teeStream() {
    const teedOff = stream.tee();
    readStream(teedOff[0], list2);
    readStream(teedOff[1], list3);
  }

Pipe chains

One very experimental feature of streams is the ability to pipe streams into one another (called a pipe chain). This involves two methods โ€” ReadableStream.pipeThrough(), which pipes a readable stream through a writer/reader pair to transform one data format into another, and ReadableStream.pipeTo(), which pipes a readable stream to a writer acting as an end point for the pipe chain.

This functionality is at a very experimental stage and is subject to change, so we have no explored it too deeply as of yet.

We have created an example called Unpack Chunks of a PNG (see it live also) that fetches an image as a stream, then pipes it through to a custom PNG transform stream that retrieves PNG chunks out of a binary data stream.

// Fetch the original image
fetch('png-logo.png')
// Retrieve its body as ReadableStream
.then(response => response.body)
// Create a gray-scaled PNG stream out of the original
.then(rs => logReadableStream('Fetch Response Stream', rs))
.then(body => body.pipeThrough(new PNGTransformStream()))
.then(rs => logReadableStream('PNG Chunk Stream', rs))

Summary

That explains the basics of โ€œdefaultโ€ readable streams. Weโ€™ll explain bytestreams in a separate future article, once they are available in browsers.