Chromium 105부터 Streams API를 사용하여 전체 본문을 사용할 수 있기 전에 요청을 시작할 수 있습니다.
다음과 같은 작업을 할 수 있습니다.
- 서버를 예열합니다. 즉, 사용자가 텍스트 입력란에 포커스를 맞추면 요청을 시작하고 모든 헤더를 숨긴 다음 사용자가 '보내기'를 누를 때까지 기다린 후 입력한 데이터를 전송할 수 있습니다.
- 오디오, 동영상, 입력 데이터와 같이 클라이언트에서 생성된 데이터를 점진적으로 전송합니다.
- HTTP/2 또는 HTTP/3을 통해 웹 소켓을 다시 만듭니다.
하지만 이는 하위 수준의 웹 플랫폼 기능이므로 제 아이디어에 국한되지 마세요. 요청 스트리밍의 훨씬 더 흥미로운 사용 사례를 생각해 보세요.
데모
여기서는 사용자로부터 서버로 데이터를 스트리밍하고 실시간으로 처리할 수 있는 데이터를 다시 전송하는 방법을 보여줍니다.
예, 가장 상상력이 풍부한 예는 아니지만 간단하게 설명하고 싶었습니다.
어쨌든 이 기능은 어떻게 작동하나요?
이전에 가져오기 스트림의 흥미로운 모험
응답 스트림은 한동안 모든 최신 브라우저에서 사용할 수 있었습니다. 이를 통해 서버에서 수신되는 응답의 일부에 액세스할 수 있습니다.
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('Received', value);
}
console.log('Response fully received');
각 value
는 바이트의 Uint8Array
입니다.
가져오는 배열의 수와 배열의 크기는 네트워크 속도에 따라 다릅니다.
연결 속도가 빠르면 더 적은 수의 더 큰 데이터 '청크'가 전송됩니다.
연결 속도가 느린 경우 더 작고 많은 청크가 전송됩니다.
바이트를 텍스트로 변환하려면 TextDecoder
를 사용하거나 대상 브라우저에서 지원하는 경우 최신 변환 스트림을 사용하면 됩니다.
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
는 이러한 모든 Uint8Array
청크를 가져와 문자열로 변환하는 변환 스트림입니다.
스트림은 데이터가 도착하는 대로 작업을 시작할 수 있으므로 유용합니다. 예를 들어 100개의 '결과' 목록을 수신하는 경우 100개가 모두 수신될 때까지 기다리지 않고 첫 번째 결과를 수신하는 즉시 표시할 수 있습니다.
어쨌든 응답 스트림은 그렇습니다. 제가 말씀드리고 싶은 흥미로운 새로운 내용은 요청 스트림입니다.
스트리밍 요청 본문
요청에 본문이 있을 수 있습니다.
await fetch(url, {
method: 'POST',
body: requestBody,
});
이전에는 요청을 시작하기 전에 전체 본문을 준비해야 했지만 이제 Chromium 105에서는 자체 데이터 ReadableStream
를 제공할 수 있습니다.
function wait(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
const stream = new ReadableStream({
async start(controller) {
await wait(1000);
controller.enqueue('This ');
await wait(1000);
controller.enqueue('is ');
await wait(1000);
controller.enqueue('a ');
await wait(1000);
controller.enqueue('slow ');
await wait(1000);
controller.enqueue('request.');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'text/plain'},
body: stream,
duplex: 'half',
});
위의 코드는 'This is a slow request'라는 문구를 서버에 한 단어씩 한 번에 하나씩 전송하며 단어 사이에 1초의 일시중지를 삽입합니다.
요청 본문의 각 청크는 바이트의 Uint8Array
여야 하므로 pipeThrough(new TextEncoderStream())
를 사용하여 변환합니다.
제한사항
스트리밍 요청은 웹의 새로운 기능이므로 몇 가지 제한사항이 있습니다.
반이중?
요청에서 스트림을 사용할 수 있도록 하려면 duplex
요청 옵션을 'half'
로 설정해야 합니다.
HTTP의 잘 알려지지 않은 기능 (표준 동작인지 여부는 묻는 사람에 따라 다름)은 요청을 전송하는 동안 응답 수신을 시작할 수 있다는 것입니다. 그러나 잘 알려지지 않아 서버에서 제대로 지원되지 않으며 어떤 브라우저에서도 지원되지 않습니다.
브라우저에서는 서버가 응답을 더 일찍 전송하더라도 요청 본문이 완전히 전송될 때까지 응답을 사용할 수 없습니다. 이는 모든 브라우저 가져오기에 적용됩니다.
이 기본 패턴을 'half duplex'라고 합니다.
그러나 Deno의 fetch
와 같은 일부 구현은 스트리밍 가져오기에 기본적으로 '풀 듀플렉스'로 설정되어 있으므로 요청이 완료되기 전에 응답을 사용할 수 있습니다.
따라서 이 호환성 문제를 해결하려면 브라우저에서 스트림 본문이 있는 요청에 duplex: 'half'
를 지정해야 합니다.
향후 브라우저에서 스트리밍 및 비스트리밍 요청을 위해 duplex: 'full'
가 지원될 수 있습니다.
그동안 듀플렉스 통신 다음으로 좋은 방법은 스트리밍 요청으로 가져오기를 한 번 한 다음 다시 가져와 스트리밍 응답을 받는 것입니다. 서버는 URL의 ID와 같이 이러한 두 요청을 연결할 방법이 필요합니다. 데모는 이러한 방식으로 작동합니다.
제한된 리디렉션
일부 HTTP 리디렉션 형식의 경우 브라우저가 요청 본문을 다른 URL로 다시 전송해야 합니다. 이를 지원하려면 브라우저에서 스트림의 콘텐츠를 버퍼링해야 하는데, 이는 목적에 맞지 않으므로 브라우저는 이를 실행하지 않습니다.
대신 요청에 스트리밍 본문이 있고 응답이 303이 아닌 HTTP 리디렉션인 경우 가져오기가 거부되고 리디렉션이 따르지않습니다.
303 리디렉션은 메서드를 GET
로 명시적으로 변경하고 요청 본문을 삭제하므로 허용됩니다.
CORS가 필요하며 프리플라이트를 트리거합니다.
스트리밍 요청에는 본문이 있지만 Content-Length
헤더는 없습니다.
이는 새로운 유형의 요청이므로 CORS가 필요하며 이러한 요청은 항상 실행 전 요청을 트리거합니다.
스트리밍 no-cors
요청은 허용되지 않습니다.
HTTP/1.x에서는 작동하지 않음
연결이 HTTP/1.x인 경우 가져오기가 거부됩니다.
HTTP/1.1 규칙에 따라 요청 및 응답 본문은 상대방이 수신할 데이터의 양을 알 수 있도록 Content-Length
헤더를 전송하거나 청크 인코딩을 사용하도록 메시지 형식을 변경해야 하기 때문입니다. 청크 인코딩을 사용하면 본문이 각기 고유한 콘텐츠 길이를 가진 부분으로 분할됩니다.
청크 인코딩은 HTTP/1.1 응답의 경우 매우 일반적이지만 요청의 경우 매우 드물기 때문에 호환성 위험이 너무 큽니다.
잠재적 문제
이 기능은 현재 인터넷에서 사용 빈도가 낮은 새로운 기능입니다. 다음은 주의해야 할 몇 가지 사안입니다.
서버 측 비호환성
일부 앱 서버는 스트리밍 요청을 지원하지 않고 대신 전체 요청이 수신될 때까지 기다린 후 요청을 표시하므로 이 방법은 그다지 효과적이지 않습니다. 대신 NodeJS 또는 Deno와 같이 스트리밍을 지원하는 앱 서버를 사용하세요.
하지만 아직 끝난 것이 아닙니다. NodeJS와 같은 애플리케이션 서버는 일반적으로 '프런트엔드 서버'라고 하는 다른 서버 뒤에 있으며, 이 서버는 CDN 뒤에 있을 수 있습니다. 이러한 중간 서버가 요청을 체인의 다음 서버에 전달하기 전에 버퍼링하기로 결정하면 요청 스트리밍의 이점을 활용할 수 없습니다.
관리 범위 외의 비호환성
이 기능은 HTTPS를 통해서만 작동하므로 개발자와 사용자 간에 프록시가 있는지 걱정할 필요는 없지만 사용자는 컴퓨터에서 프록시를 실행하고 있을 수 있습니다. 일부 인터넷 보호 소프트웨어는 브라우저와 네트워크 간에 전송되는 모든 항목을 모니터링할 수 있도록 이를 수행하며, 이 소프트웨어에서 요청 본문을 버퍼링하는 경우도 있습니다.
이를 방지하려면 스트림을 닫지 않고 일부 데이터를 스트리밍하려고 시도하는 위의 데모와 유사한 '기능 테스트'를 만들 수 있습니다. 서버가 데이터를 수신하면 다른 가져오기를 통해 응답할 수 있습니다. 이 경우 클라이언트가 스트리밍 요청을 엔드 투 엔드로 지원한다는 것을 알 수 있습니다.
기능 감지
const supportsRequestStreams = (() => {
let duplexAccessed = false;
const hasContentType = new Request('', {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
})();
if (supportsRequestStreams) {
// …
} else {
// …
}
기능 감지 작동 방식을 궁금하다면 다음을 참고하세요.
브라우저가 특정 body
유형을 지원하지 않는 경우 객체에서 toString()
를 호출하고 결과를 본문으로 사용합니다.
따라서 브라우저가 요청 스트림을 지원하지 않으면 요청 본문이 문자열 "[object ReadableStream]"
이 됩니다.
문자열이 본문으로 사용되면 편리하게 Content-Type
헤더를 text/plain;charset=UTF-8
로 설정합니다.
따라서 이 헤더가 설정되면 브라우저가 요청 객체의 스트림을 지원하지 않는 것을 알 수 있으므로 조기에 종료할 수 있습니다.
Safari는 요청 객체의 스트림을 지원하지만 fetch
와 함께 사용할 수는 없습니다. 따라서 현재 Safari에서 지원하지 않는 duplex
옵션이 테스트됩니다.
쓰기 가능한 스트림과 함께 사용
WritableStream
가 있으면 스트림을 사용하는 것이 더 쉬울 때가 있습니다.
'ID' 스트림을 사용하면 됩니다. 'ID' 스트림은 쓰기 가능한 쪽에 전달된 모든 항목을 가져와 읽기 가능한 쪽으로 전송하는 읽기 가능/쓰기 가능 쌍입니다.
인수 없이 TransformStream
를 만들어 다음 중 하나를 만들 수 있습니다.
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
이제 쓰기 가능한 스트림에 전송하는 모든 항목이 요청의 일부가 됩니다. 이렇게 하면 스트림을 함께 구성할 수 있습니다. 예를 들어 다음은 데이터가 한 URL에서 가져와 압축된 후 다른 URL로 전송되는 어리석은 예입니다.
// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();
// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);
// Post to url2:
await fetch(url2, {
method: 'POST',
body: readable,
});
위 예에서는 압축 스트림을 사용하여 gzip을 통해 임의의 데이터를 압축합니다.