Skip to content

스트림의 역압

데이터 처리 중에 발생하는 일반적인 문제로 역압(backpressure)이 있으며, 데이터 전송 중 버퍼 뒤에 데이터가 쌓이는 현상을 설명합니다. 전송 수신 측에서 복잡한 작업을 수행하거나 어떤 이유로든 속도가 느리면 유입되는 소스의 데이터가 막힌 것처럼 축적되는 경향이 있습니다.

이 문제를 해결하려면 한 소스에서 다른 소스로 데이터가 원활하게 흐르도록 보장하는 위임 시스템이 마련되어야 합니다. 다양한 커뮤니티에서 프로그램에 고유하게 이 문제를 해결했으며, Unix 파이프와 TCP 소켓이 좋은 예이며, 종종 흐름 제어라고도 합니다. Node.js에서는 스트림이 채택된 솔루션입니다.

이 가이드의 목적은 역압이 무엇인지, 그리고 Node.js 소스 코드에서 스트림이 이 문제를 정확히 어떻게 처리하는지 자세히 설명하는 것입니다. 가이드의 두 번째 부분에서는 스트림을 구현할 때 애플리케이션 코드가 안전하고 최적화되도록 하기 위한 권장 모범 사례를 소개합니다.

Node.js에서 backpressure, BufferEventEmitters의 일반적인 정의와 Stream 사용 경험이 조금 있다고 가정합니다. 해당 문서를 읽어보지 않았다면 API 문서를 먼저 살펴보는 것이 좋습니다. 이 가이드를 읽으면서 이해를 넓히는 데 도움이 될 것입니다.

데이터 처리의 문제점

컴퓨터 시스템에서 데이터는 파이프, 소켓 및 신호를 통해 한 프로세스에서 다른 프로세스로 전송됩니다. Node.js에서는 Stream이라는 유사한 메커니즘이 있습니다. 스트림은 훌륭합니다! Node.js에 많은 기능을 제공하며 내부 코드베이스의 거의 모든 부분에서 해당 모듈을 활용합니다. 개발자로서 여러분도 스트림을 사용하는 것이 좋습니다!

javascript
const readline = require('node:readline')

const rl = readline.createInterface({
  output: process.stdout,
  input: process.stdin,
})

rl.question('스트림을 사용해야 하는 이유는 무엇입니까? ', answer => {
  console.log(`아마 ${answer} 때문일 수도 있고, 아마 스트림이 멋지기 때문일 수도 있습니다!`)
})

rl.close()

스트림을 통해 구현된 역압 메커니즘이 왜 훌륭한 최적화인지에 대한 좋은 예는 Node.js의 스트림 구현의 내부 시스템 도구를 비교하여 보여줄 수 있습니다.

한 시나리오에서는 큰 파일(약 -9GB)을 가져와 익숙한 zip(1) 도구를 사용하여 압축합니다.

bash
zip The.Matrix.1080p.mkv

완료하는 데 몇 분이 걸리지만, 다른 셸에서는 또 다른 압축 도구인 gzip(1)을 감싸는 Node.js 모듈 zlib을 사용하는 스크립트를 실행할 수 있습니다.

javascript
const gzip = require('node:zlib').createGzip()
const fs = require('node:fs')

const inp = fs.createReadStream('The.Matrix.1080p.mkv')
const out = fs.createWriteStream('The.Matrix.1080p.mkv.gz')

inp.pipe(gzip).pipe(out)

결과를 테스트하려면 각 압축 파일을 열어보세요. zip(1) 도구로 압축된 파일은 파일이 손상되었다고 알려주는 반면, 스트림으로 완료된 압축은 오류 없이 압축이 해제됩니다.

참고

이 예에서는 .pipe()를 사용하여 한쪽 끝에서 다른 쪽 끝으로 데이터 소스를 가져옵니다. 그러나 연결된 적절한 오류 처리기가 없습니다. 데이터 청크를 제대로 받지 못하면 읽기 가능한 소스 또는 gzip 스트림이 소멸되지 않습니다. pump는 파이프라인의 스트림 중 하나가 실패하거나 닫히면 파이프라인의 모든 스트림을 올바르게 소멸시키는 유틸리티 도구이며, 이 경우에는 반드시 있어야 합니다!

pump는 Node.js 8.x 이하 버전에서만 필요하며, Node.js 10.x 이상 버전에서는 pump를 대체하기 위해 pipeline이 도입되었습니다. 이것은 오류를 전달하고 파이프라인이 완료될 때 적절하게 정리하고 콜백을 제공하는 스트림 간에 파이프하는 모듈 메서드입니다.

다음은 파이프라인을 사용하는 예입니다.

javascript
const { pipeline } = require('node:stream')
const fs = require('node:fs')
const zlib = require('node:zlib')
// 파이프라인 API를 사용하여 일련의 스트림을 쉽게 파이프할 수 있습니다.
// 함께 연결하고 파이프라인이 완전히 완료되면 알림을 받습니다.
// 잠재적으로 거대한 비디오 파일을 효율적으로 gzip하는 파이프라인:
pipeline(
  fs.createReadStream('The.Matrix.1080p.mkv'),
  zlib.createGzip(),
  fs.createWriteStream('The.Matrix.1080p.mkv.gz'),
  err => {
    if (err) {
      console.error('파이프라인 실패', err)
    } else {
      console.log('파이프라인 성공')
    }
  }
)

stream/promises 모듈을 사용하여 async / await와 함께 파이프라인을 사용할 수도 있습니다.

javascript
const { pipeline } = require('node:stream/promises')
const fs = require('node:fs')
const zlib = require('node:zlib')
async function run() {
  try {
    await pipeline(
      fs.createReadStream('The.Matrix.1080p.mkv'),
      zlib.createGzip(),
      fs.createWriteStream('The.Matrix.1080p.mkv.gz')
    )
    console.log('파이프라인 성공')
  } catch (err) {
    console.error('파이프라인 실패', err)
  }
}

너무 많은 데이터, 너무 빠른 속도

Readable 스트림이 Writable에 데이터를 너무 빠르게 제공하는 경우가 있습니다. 즉, 소비자가 처리할 수 있는 것보다 훨씬 더 많은 데이터를 제공하는 경우입니다!

이러한 상황이 발생하면 소비자는 나중에 소비하기 위해 모든 데이터 청크를 대기열에 넣기 시작합니다. 쓰기 대기열이 점점 길어지고 이로 인해 전체 프로세스가 완료될 때까지 더 많은 데이터를 메모리에 보관해야 합니다.

디스크에 쓰는 속도는 디스크에서 읽는 속도보다 훨씬 느립니다. 따라서 파일을 압축하여 하드 디스크에 쓰려고 할 때 읽기 속도를 쓰기 속도가 따라가지 못해 백프레셔가 발생합니다.

javascript
// 스트림은 몰래 다음과 같이 말합니다. "워워! 잠깐만요, 이건 너무 많아요!"
// 데이터가 들어오는 흐름을 따라가려고 write가 노력하면서
// 데이터 버퍼의 읽기 측에 데이터가 쌓이기 시작합니다.
inp.pipe(gzip).pipe(outputFile)

이것이 백프레셔 메커니즘이 중요한 이유입니다. 백프레셔 시스템이 없으면 프로세스가 시스템 메모리를 모두 사용하게 되어 다른 프로세스 속도를 늦추고 완료될 때까지 시스템의 상당 부분을 독점합니다.

이로 인해 다음과 같은 몇 가지 문제가 발생합니다.

  • 현재 실행 중인 다른 모든 프로세스 속도 저하
  • 과도하게 사용되는 가비지 컬렉터
  • 메모리 소진

다음 예에서는 .write() 함수의 반환 값을 가져와서 true로 변경하여 Node.js 코어에서 백프레셔 지원을 효과적으로 비활성화합니다. 'modified' 바이너리에 대한 언급은 return ret; 줄 없이 노드 바이너리를 실행하고 대신 return true;로 대체한 경우를 말합니다.

가비지 컬렉션에 대한 과도한 부담

간단한 벤치마크를 살펴보겠습니다. 위와 동일한 예제를 사용하여 두 바이너리에 대한 중간 시간을 얻기 위해 몇 가지 시간 테스트를 실행했습니다.

bash
   trial (#)  | `node` 바이너리 (ms) | modified `node` 바이너리 (ms)
=================================================================
      1       |      56924         |           55011
      2       |      52686         |           55869
      3       |      59479         |           54043
      4       |      54473         |           55229
      5       |      52933         |           59723
=================================================================
평균 시간: |      55299         |           55975

둘 다 실행하는 데 약 1분 정도 걸리므로 별 차이가 없지만, 의심이 맞는지 확인하기 위해 더 자세히 살펴보겠습니다. V8 가비지 컬렉터에서 무슨 일이 일어나고 있는지 평가하기 위해 Linux 도구인 dtrace를 사용합니다.

GC(가비지 컬렉터) 측정 시간은 가비지 컬렉터가 수행하는 단일 스위프의 전체 사이클 간격을 나타냅니다.

bash
approx. time (ms) | GC (ms) | modified GC (ms)
=================================================
          0       |    0    |      0
          1       |    0    |      0
         40       |    0    |      2
        170       |    3    |      1
        300       |    3    |      1
         *             *           *
         *             *           *
         *             *           *
      39000       |    6    |     26
      42000       |    6    |     21
      47000       |    5    |     32
      50000       |    8    |     28
      54000       |    6    |     35

두 프로세스가 동일하게 시작되고 GC를 동일한 속도로 작동시키는 것처럼 보이지만, 적절하게 작동하는 백프레셔 시스템이 몇 초 지나면 데이터 전송이 끝날 때까지 일관된 4-8밀리초 간격으로 GC 부하를 분산시키는 것이 분명해집니다.

그러나 백프레셔 시스템이 없으면 V8 가비지 컬렉션이 지연되기 시작합니다. 일반 바이너리는 1분 안에 약 75회 GC를 호출하는 반면 수정된 바이너리는 36회만 호출합니다.

이것이 늘어나는 메모리 사용량으로 인해 누적되는 느리고 점진적인 부채입니다. 백프레셔 시스템 없이 데이터가 전송되면 각 청크 전송에 더 많은 메모리가 사용됩니다.

할당되는 메모리가 많을수록 GC가 한 번의 스위프에서 더 많이 처리해야 합니다. 스위프가 클수록 GC가 해제할 수 있는 항목을 결정해야 하고 더 큰 메모리 공간에서 분리된 포인터를 검색하는 데 더 많은 컴퓨팅 성능이 소비됩니다.

메모리 소모

각 바이너리의 메모리 소비량을 확인하기 위해, 각 프로세스를 /usr/bin/time -lp sudo ./node ./backpressure-example/zlib.js 명령어로 개별적으로 측정했습니다.

다음은 일반 바이너리의 출력입니다.

bash
.write()의 반환 값을 준수하는 경우
=============================================
real        58.88
user        56.79
sys          8.79
  87810048  최대 상주 집합 크기
         0  평균 공유 메모리 크기
         0  평균 비공유 데이터 크기
         0  평균 비공유 스택 크기
     19427  페이지 재확보
      3134  페이지 폴트
         0  스왑
         5  블록 입력 작업
       194  블록 출력 작업
         0  메시지 전송
         0  메시지 수신
         1  시그널 수신
        12  자발적 문맥 전환
    666037  비자발적 문맥 전환

가상 메모리에 의해 점유된 최대 바이트 크기는 약 87.81MB로 나타났습니다.

이제 .write() 함수의 반환 값을 변경하면 다음과 같은 결과를 얻습니다.

bash
.write()의 반환 값을 준수하지 않는 경우:
==================================================
real        54.48
user        53.15
sys          7.43
1524965376  최대 상주 집합 크기
         0  평균 공유 메모리 크기
         0  평균 비공유 데이터 크기
         0  평균 비공유 스택 크기
    373617  페이지 재확보
      3139  페이지 폴트
         0  스왑
        18  블록 입력 작업
       199  블록 출력 작업
         0  메시지 전송
         0  메시지 수신
         1  시그널 수신
        25  자발적 문맥 전환
    629566  비자발적 문맥 전환

가상 메모리에 의해 점유된 최대 바이트 크기는 약 1.52GB로 나타났습니다.

백프레셔를 위임하는 스트림이 없으면 할당되는 메모리 공간이 훨씬 더 커집니다. 동일한 프로세스에서 엄청난 차이가 발생합니다!

이 실험은 Node.js의 백프레셔 메커니즘이 컴퓨팅 시스템에 얼마나 최적화되고 비용 효율적인지 보여줍니다. 이제 작동 방식을 자세히 살펴보겠습니다!

백프레셔는 이러한 문제를 어떻게 해결합니까?

한 프로세스에서 다른 프로세스로 데이터를 전송하는 여러 가지 기능이 있습니다. Node.js에는 .pipe()라는 내장 기능이 있습니다. 다른 패키지도 사용할 수 있습니다! 궁극적으로 이 프로세스의 기본 수준에서 데이터 소스와 소비자라는 두 개의 개별 구성 요소가 있습니다.

소스에서 .pipe()가 호출되면 소비자에게 전송할 데이터가 있음을 알립니다. 파이프 함수는 이벤트 트리거에 적절한 백프레셔 클로저를 설정하는 데 도움이 됩니다.

Node.js에서 소스는 Readable 스트림이고 소비자는 Writable 스트림입니다(둘 다 Duplex 또는 Transform 스트림으로 교환할 수 있지만 이 가이드의 범위를 벗어남).

백프레셔가 트리거되는 순간은 Writable.write() 함수의 반환 값으로 정확히 좁힐 수 있습니다. 이 반환 값은 물론 몇 가지 조건에 의해 결정됩니다.

데이터 버퍼가 highwaterMark를 초과했거나 쓰기 대기열이 현재 사용 중인 모든 시나리오에서 .write()false를 반환합니다.

false 값이 반환되면 백프레셔 시스템이 작동합니다. 들어오는 Readable 스트림이 데이터를 보내지 않도록 일시 중지하고 소비자가 다시 준비될 때까지 기다립니다. 데이터 버퍼가 비워지면 'drain' 이벤트가 발생하고 들어오는 데이터 흐름이 재개됩니다.

대기열이 완료되면 백프레셔는 데이터를 다시 보낼 수 있도록 합니다. 사용 중이던 메모리의 공간이 해제되고 다음 데이터 배치를 준비합니다.

이를 통해 .pipe() 함수에 대해 주어진 시간에 고정된 양의 메모리를 효과적으로 사용할 수 있습니다. 메모리 누출이나 무한 버퍼링이 없으며 가비지 컬렉터는 메모리의 한 영역만 처리하면 됩니다!

그렇다면 백프레셔가 그렇게 중요하다면 왜 (아마도) 들어본 적이 없을까요? 글쎄, 답은 간단합니다. Node.js가 이 모든 것을 자동으로 처리하기 때문입니다.

정말 좋죠! 하지만 사용자 지정 스트림을 구현하는 방법을 이해하려고 할 때는 그다지 좋지 않습니다.

참고

대부분의 기기에는 버퍼가 가득 찼는지 여부를 결정하는 바이트 크기가 있습니다(기기마다 다름). Node.js에서는 사용자 정의 highWaterMark를 설정할 수 있지만 일반적으로 기본값은 16kb(16384 또는 objectMode 스트림의 경우 16)로 설정됩니다. 해당 값을 높여야 할 경우도 있지만 주의해서 수행해야 합니다!

.pipe()의 라이프사이클

역압에 대한 더 나은 이해를 위해, Writable 스트림으로 파이프되는 Readable 스트림의 라이프사이클에 대한 흐름도를 소개합니다.

bash
                                                     +===================+
                         x-->  파이핑 함수는    +-->   src.pipe(dest)  |
                         x     .pipe 메서드 동안      |===================|
                         x     설정됩니다.        |  이벤트 콜백      |
  +===============+      x                           |-------------------|
  |   데이터    |      x     데이터 흐름 외부에서   | .on('close', cb)  |
  +=======+=======+      x     존재하지만, 중요하게  | .on('data', cb)   |
          |              x     이벤트를 첨부하고,   | .on('drain', cb)  |
          |              x     각각의 콜백을 첨부합니다.  | .on('unpipe', cb) |
+---------v---------+    x                           | .on('error', cb)  |
|  읽기 가능 스트림  +----+                           | .on('finish', cb) |
+-^-------^-------^-+    |                           | .on('end', cb)    |
  ^       |       ^      |                           +-------------------+
  |       |       |      |
  |       ^       |      |
  ^       ^       ^      |    +-------------------+         +=================+
  ^       |       ^      +---->  쓰기 가능 스트림  +--------->  .write(chunk)  |
  |       |       |           +-------------------+         +=======+=========+
  |       |       |                                                 |
  |       ^       |                              +------------------v---------+
  ^       |       +-> if (!chunk)                | chunk가 너무 큰가?  |
  ^       |       |     .end()을 내보냅니다.        |    큐가 사용 중인가?      |
  |       |       +-> else                       +-------+----------------+---+
  |       ^       |     .write()을 내보냅니다.              |                |
  |       ^       ^                                   +--v---+        +---v---+
  |       |       ^-----------------------------------<  아니오 |        |  |
  ^       |                                           +------+        +---v---+
  ^       |                                                               |
  |       ^               .pause() 내보냅니다.  +=================+     |
  |       ^---------------^-----------------------+  false 반환;   <-----+---+
  |                                               +=================+         |
  |                                                                           |
  ^            큐가 비어 있을        +============+                         |
  ^------------^-----------------------<  버퍼링 |                         |
               |                       |============|                         |
               +> .drain() 내보냅니다.       |  ^버퍼^  |                         |
               +> .resume() 내보냅니다.      +------------+                         |
                                       |  ^버퍼^  |                         |
                                       +------------+   chunk를 큐에 추가   |
                                       |            <---^---------------------<
                                       +============+

참고

데이터를 조작하기 위해 몇 개의 스트림을 함께 연결하는 파이프라인을 설정하는 경우, 변환(Transform) 스트림을 구현할 가능성이 큽니다.

이 경우 Readable 스트림의 출력은 Transform에 입력되고 Writable로 파이프됩니다.

javascript
Readable.pipe(Transformable).pipe(Writable)

역압은 자동으로 적용되지만, Transform 스트림의 들어오는 및 나가는 highwaterMark 모두 조작할 수 있으며 역압 시스템에 영향을 미칩니다.

백프레셔 지침

Node.js v0.10부터 Stream 클래스는 해당 함수(._read()._write())의 밑줄 버전을 사용하여 .read() 또는 .write()의 동작을 수정하는 기능을 제공합니다.

Readable 스트림 구현 및 Writable 스트림 구현에 대한 문서화된 지침이 있습니다. 이러한 내용을 읽었다고 가정하고 다음 섹션에서는 좀 더 자세히 살펴보겠습니다.

사용자 정의 스트림 구현 시 준수해야 할 규칙

스트림의 황금률은 항상 백프레셔를 존중하는 것입니다. 가장 좋은 방법은 모순되지 않는 방법입니다. 내부 백프레셔 지원과 충돌하는 동작을 피하도록 주의하는 한 좋은 방법을 따르고 있다고 확신할 수 있습니다.

일반적으로,

  1. 요청받지 않은 경우 .push()를 사용하지 마십시오.
  2. false를 반환한 후에는 .write()를 호출하지 말고 대신 'drain'을 기다리십시오.
  3. 스트림은 Node.js 버전과 사용하는 라이브러리에 따라 변경됩니다. 주의하고 테스트하십시오.

참고

3항과 관련하여 브라우저 스트림을 구축하기 위한 매우 유용한 패키지는 readable-stream입니다. Rodd Vagg는 이 라이브러리의 유용성을 설명하는 훌륭한 블로그 게시물을 작성했습니다. 간단히 말해서, Readable 스트림에 대한 일종의 자동화된 정상적인 성능 저하를 제공하고 이전 버전의 브라우저와 Node.js를 지원합니다.

Readable 스트림에 특정한 규칙

지금까지 우리는 .write()가 백프레셔에 어떻게 영향을 미치는지 살펴보고 Writable 스트림에 많은 초점을 맞췄습니다. Node.js의 기능으로 인해 데이터는 기술적으로 Readable에서 Writable로 다운스트림으로 흐릅니다. 그러나 데이터, 물질 또는 에너지 전송에서 관찰할 수 있듯이 소스는 대상만큼 중요하며 Readable 스트림은 백프레셔가 처리되는 방식에 매우 중요합니다.

이러한 두 프로세스는 효과적으로 통신하기 위해 서로 의존하며, Writable 스트림이 데이터 전송 중지를 요청할 때 Readable이 이를 무시하면 .write()의 반환 값이 잘못되었을 때만큼 문제가 될 수 있습니다.

따라서 .write() 반환을 존중하는 것 외에도 ._read() 메서드에서 사용되는 .push()의 반환 값도 존중해야 합니다. .push()가 false 값을 반환하면 스트림이 소스에서 읽기를 중단합니다. 그렇지 않으면 일시 중지 없이 계속됩니다.

다음은 .push()를 사용한 잘못된 방법의 예입니다.

javascript
// 이것은 대상 스트림의 백프레셔 신호일 수 있는 푸시의 반환 값을 완전히 무시하기 때문에 문제가 있습니다!
class MyReadable extends Readable {
  _read(size) {
    let chunk
    while (null == (chunk = getNextChunk())) {
      this.push(chunk)
    }
  }
}

또한 사용자 정의 스트림 외부에서 백프레셔를 무시하면 함정이 있습니다. 좋은 방법의 반례인 이 예에서 응용 프로그램의 코드는 사용 가능한 데이터( 'data' 이벤트로 신호됨)를 통해 강제로 데이터를 전달합니다.

javascript
// 이는 Node.js가 설정한 백프레셔 메커니즘을 무시하고,
// 대상 스트림이 준비되었는지 여부에 관계없이 데이터를 무조건적으로 전달합니다.
readable.on('data', data => writable.write(data))

다음은 Readable 스트림과 함께 .push()를 사용하는 예입니다.

javascript
const { Readable } = require('node:stream')

// 사용자 정의 Readable 스트림 생성
const myReadableStream = new Readable({
  objectMode: true,
  read(size) {
    // 스트림에 일부 데이터 푸시
    this.push({ message: 'Hello, world!' })
    this.push(null) // 스트림의 끝을 표시
  },
})

// 스트림 소비
myReadableStream.on('data', chunk => {
  console.log(chunk)
})

// 출력:
// { message: 'Hello, world!' }

Writable 스트림에 특화된 규칙

.write()는 특정 조건에 따라 true 또는 false를 반환할 수 있습니다. 다행히도, 자체 Writable 스트림을 빌드할 때 스트림 상태 머신이 콜백을 처리하고 백프레셔를 처리하는 시기를 결정하여 데이터 흐름을 최적화합니다. 그러나 Writable을 직접 사용하려는 경우 .write() 반환 값을 존중하고 다음 조건에 주의해야 합니다.

  • 쓰기 큐가 사용 중이면 .write()는 false를 반환합니다.
  • 데이터 청크가 너무 크면 .write()는 false를 반환합니다(제한은 highWaterMark 변수로 표시됨).

이 예제에서는 .push()를 사용하여 스트림에 단일 객체를 푸시하는 사용자 정의 Readable 스트림을 만듭니다. 스트림이 데이터를 소비할 준비가 되면 ._read() 메서드가 호출되며, 이 경우 즉시 일부 데이터를 스트림에 푸시하고 null을 푸시하여 스트림의 끝을 표시합니다.

javascript
const stream = require('stream')

class MyReadable extends stream.Readable {
  constructor() {
    super()
  }

  _read() {
    const data = { message: 'Hello, world!' }
    this.push(data)
    this.push(null)
  }
}

const readableStream = new MyReadable()

readableStream.pipe(process.stdout)

그런 다음 'data' 이벤트를 수신하고 스트림에 푸시되는 각 데이터 청크를 로깅하여 스트림을 소비합니다. 이 경우 스트림에 단일 데이터 청크만 푸시하므로 하나의 로그 메시지만 표시됩니다.

Writable 스트림에 특화된 규칙

.write()는 특정 조건에 따라 true 또는 false를 반환할 수 있습니다. 다행히도 자체 Writable 스트림을 빌드할 때 스트림 상태 머신이 콜백을 처리하고 백프레셔를 처리하는 시기를 결정하여 데이터 흐름을 최적화합니다.

그러나 Writable을 직접 사용하려는 경우 .write() 반환 값을 존중하고 다음 조건에 주의해야 합니다.

  • 쓰기 큐가 사용 중이면 .write()는 false를 반환합니다.
  • 데이터 청크가 너무 크면 .write()는 false를 반환합니다(제한은 highWaterMark 변수로 표시됨).
javascript
class MyWritable extends Writable {
  // 이 쓰기 가능은 JavaScript 콜백의 비동기 특성 때문에 유효하지 않습니다.
  // 마지막 이전의 각 콜백에 대한 반환 문이 없으면
  // 여러 콜백이 호출될 가능성이 큽니다.
  write(chunk, encoding, callback) {
    if (chunk.toString().indexOf('a') >= 0) callback()
    else if (chunk.toString().indexOf('b') >= 0) callback()
    callback()
  }
}

._writev()를 구현할 때 주의해야 할 몇 가지 사항도 있습니다. 이 함수는 .cork()와 결합되지만 작성 시 흔히 발생하는 실수가 있습니다.

javascript
// 여기서 .uncork()를 두 번 사용하면 C++ 레이어에서 두 번 호출하여
// 코르크/언코르크 기술이 쓸모없게 됩니다.
ws.cork()
ws.write('hello ')
ws.write('world ')
ws.uncork()

ws.cork()
ws.write('from ')
ws.write('Matteo')
ws.uncork()

// 이렇게 작성하는 올바른 방법은 다음 이벤트 루프에서 발생하는
// process.nextTick()을 사용하는 것입니다.
ws.cork()
ws.write('hello ')
ws.write('world ')
process.nextTick(doUncork, ws)

ws.cork()
ws.write('from ')
ws.write('Matteo')
process.nextTick(doUncork, ws)

// 전역 함수로.
function doUncork(stream) {
  stream.uncork()
}

.cork()는 원하는 만큼 여러 번 호출할 수 있지만 다시 흐르게 하려면 .uncork()를 같은 횟수만큼 호출해야 합니다.

결론

스트림은 Node.js에서 자주 사용되는 모듈입니다. 이는 내부 구조에 중요하며, 개발자가 Node.js 모듈 생태계 전반에 걸쳐 확장하고 연결하는 데에도 중요합니다.

이제 백프레셔를 염두에 두고 자신만의 WritableReadable 스트림을 안전하게 코딩하고 문제를 해결할 수 있으며, 동료 및 친구와 지식을 공유할 수 있기를 바랍니다.

Node.js로 애플리케이션을 구축할 때 스트리밍 기능을 개선하고 활용하는 데 도움이 되는 다른 API 함수에 대해 Stream을 자세히 읽어보십시오.