Node.js HTTP/2 및 HTTP/3로 현대 웹 강화
Wenhao Wang
Dev Intern · Leapcell

소개
웹의 진화는 속도와 효율성을 끊임없이 추구해왔습니다. 단순함에도 불구하고 헤드-오브-라인 차단과 같은 심각한 병목 현상을 초래했던 HTTP/1.x 시절부터 우리는 지속적으로 더 나은 성능 지표를 위해 노력해왔습니다. 이러한 노력은 HTTP/2 및 HTTP/3(QUIC)과 같은 최첨단 프로토콜의 개발로 이어졌습니다. Node.js를 사용하는 JavaScript 개발자에게 이러한 프로토콜을 이해하고 활용하는 것은 더 이상 선택 사항이 아니라 응답성이 뛰어나고 확장 가능한 웹 애플리케이션을 구축하는 데 매우 중요한 측면입니다. HTTP/2 및 HTTP/3는 더 빠른 페이지 로드, 지연 시간 감소, 모바일 경험 개선과 같은 상당한 성능 이점을 제공하며, 이는 사용자 참여도 및 운영 효율성 향상으로 직접 이어집니다. 이 글에서는 Node.js의 이러한 고급 프로토콜에 대한 내재된 지원을 살펴보고, 기본 메커니즘, 실질적인 구현 및 실제 애플리케이션을 탐구하여 개발자가 현대 웹 통신의 잠재력을 최대한 발휘할 수 있도록 합니다.
최신 웹 프로토콜 이해
Node.js의 구체적인 내용에 들어가기 전에 논의할 핵심 프로토콜을 간략하게 정의해 보겠습니다.
-
HTTP/1.1: 널리 사용되지만 비효율성으로 알려진 기본 프로토콜입니다. 요청을 순차적으로 처리하므로 동일한 연결에서 후속 요청을 지연시킬 수 있는 "헤드-오브-라인 차단" 문제가 발생합니다. 각 리소스(이미지, 스크립트, CSS)는 종종 별도의 TCP 연결을 필요로 하여 상당한 오버헤드가 발생합니다.
-
HTTP/2: 핵심 의미론을 깨뜨리지 않고 HTTP/1.1의 단점을 해결하기 위해 도입되었습니다. 주요 기능은 다음과 같습니다.
- 다중화(Multiplexing): 단일 TCP 연결을 통해 여러 요청과 응답을 동시에 보낼 수 있어 애플리케이션 계층에서 헤드-오브-라인 차단을 제거합니다.
- 서버 푸시(Server Push): 서버는 클라이언트가 필요할 것으로 예상하는 리소스를 클라이언트에 미리 보낼 수 있어 왕복 시간을 줄입니다.
- 헤더 압축(HPACK): 종종 반복되는 HTTP 헤더의 오버헤드를 줄입니다.
- 우선순위 지정( Prioritization): 클라이언트는 어떤 리소스가 더 중요한지 신호를 보낼 수 있어 서버가 전송 우선순위를 지정할 수 있습니다.
- 이진 프레이밍 계층(Binary Framing Layer): HTTP/2는 이진 프로토콜로 작동하므로 텍스트 기반 이전 프로토콜보다 파싱하고 전송하기 더 효율적입니다.
-
HTTP/3 (QUIC): TCP 대신 QUIC(Quick UDP Internet Connections)을 기반으로 구축된 HTTP의 최신 버전입니다. 이러한 근본적인 변화는 여러 가지 이점을 가져옵니다.
- UDP 기반: 특히 전송 계층의 헤드-오브-라인 차단과 관련된 TCP의 고유한 제한 사항을 우회합니다. 패킷 하나가 손실되면 연결의 다른 모든 스트림에 영향을 미치지 않고 해당 스트림만 영향을 받습니다.
- 통합 TLS 1.3 암호화: QUIC는 거의 모든 헤더를 암호화하여 개인 정보 보호 및 보안을 강화하며, TLS 핸드셰이크를 연결 설정에 통합하여 연결 설정을 더 빠르게 만듭니다.
- 지연 시간 감소 연결 설정: 후속 연결 및 초기 연결에 대해 각각 0-RTT(제로 왕복 시간) 또는 1-RTT 연결을 달성하여 초기 페이지 로드를 크게 단축합니다.
- 향상된 연결 마이그레이션: 연결은 IP 주소 변경(예: Wi-Fi에서 셀룰러로 전환)을 통해 지속될 수 있어 사용자 경험이 더 부드러워집니다.
Node.js의 HTTP/2 및 HTTP/3 지원
Node.js는 HTTP/2 및 HTTP/3 모두에 대한 강력한 네이티브 지원을 제공하여 개발자가 최소한의 외부 종속성으로 이러한 프로토콜을 활용할 수 있도록 합니다.
Node.js의 HTTP/2
Node.js는 버전 8.8.1부터 내장 http2 모듈을 통해 네이티브 HTTP/2 지원을 제공했습니다. 두 가지 모드로 작동할 수 있습니다.
- 보안(HTTPS/2): TLS를 사용하여 암호화를 활용하는 가장 일반적이고 권장되는 방법입니다.
- 비보안(사전 인식 HTTP/2): TLS 오버헤드를 명시적으로 피하는 특정 로컬 또는 내부 네트워크 설정에 사용되는 덜 일반적인 방법입니다.
간단한 HTTP/2 서버를 만드는 예제를 살펴보겠습니다.
// server.js const http2 = require('http2'); const fs = require('fs'); // 보안 HTTP/2에는 서버 인증서가 필요합니다. const options = { key: fs.readFileSync('server.key'), cert: fs.readFileSync('server.cert'), }; const server = http2.createSecureServer(options); server.on('stream', (stream, headers) => { const path = headers[':path']; console.log(`요청 수신: ${path}`); if (path === '/') { // 서버 푸시 예제: HTML이 완전히 전송되기 전에 CSS 푸시 stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => { if (err) throw err; pushStream.writeHead(200, { 'Content-Type': 'text/css' }); pushStream.end('body { font-family: sans-serif; background-color: #f0f0f0; }'); console.log('Pushed /style.css'); }); stream.writeHead(200, { 'Content-Type': 'text/html' }); stream.end(` <html> <head> <title>Node.js HTTP/2 Server</title> <link rel="stylesheet" href="/style.css"> </head> <body> <h1>Welcome to HTTP/2!</h1> <p>This page was served over HTTP/2, with CSS pushed by the server.</p> </body> </html> `); } else if (path === '/style.css') { // 이 블록은 클라이언트가 명시적으로 /style.css를 요청한 후 (푸시된 경우) 또는 클라이언트가 푸시를 무시하기로 선택한 경우에만 실행될 수 있습니다. stream.writeHead(200, { 'Content-Type': 'text/css' }); stream.end('body { font-family: sans-serif; background-color: #f0f0f0; }'); console.log('Served /style.css (explicitly)'); } else { stream.writeHead(404); stream.end('Not Found'); } }); server.on('error', (err) => console.error(err)); server.listen(8443, () => { console.log('HTTP/2 server listening on https://localhost:8443'); console.log('먼저 자체 서명된 인증서를 생성해야 합니다:'); console.log('openssl genrsa -out server.key 2048'); console.log('openssl req -new -x509 -key server.key -out server.cert -days 365'); });
이를 실행하려면 자체 서명된 SSL 인증서(server.key 및 server.cert)가 필요합니다. OpenSSL을 사용하여 생성할 수 있습니다.
openssl genrsa -out server.key 2048 openssl req -new -x509 -key server.key -out server.cert -days 365
최신 브라우저에서 https://localhost:8443에 액세스하면 서버 푸시 덕분에 /style.css가 브라우저가 명시적으로 요청하기 전에 다운로드되는 경우가 많다는 것을 알 수 있습니다.
Node.js의 HTTP/3 (QUIC)
Node.js는 Node.js 15에서 HTTP/3(QUIC)에 대한 실험적인 지원을 도입했습니다. quic 모듈(node:quic의 일부)은 HTTP/3를 포함한 QUIC 애플리케이션 구축을 위한 기본 요소를 제공합니다. Node.js 18 이상에서는 QUIC 구현이 더 성숙했지만 여전히 실험적이거나 플래그가 필요한 경우가 많습니다.
Htttp/3에 대한 Node.js의 API는 node:quic 모듈을 기반으로 하며 http2 모듈의 일부 측면을 반영하고 세션 및 스트림을 처리합니다. QUIC는 UDP 기반이므로 http 및 http2 서버(TCP 기반)와 설정이 약간 다릅니다.
다음은 기본적인 HTTP/3 서버 예제입니다. QUIC는 기본적으로 항상 암호화되므로 인증서는 필수입니다.
// server-quic.js const { createQuicSocket } = require('node:quic'); const fs = require('fs'); const key = fs.readFileSync('server.key'); const cert = fs.readFileSync('server.cert'); const server = createQuicSocket({ type: 'udp4', // IPv4 전송 port: 8443, lookup: (hostname, options, callback) => { // localhost에 대한 사용자 지정 조회 callback(null, [ { address: '127.0.0.1', family: 4, hostname: 'localhost' } ]); } }); // 새로운 세션을 수신 대기하도록 QUIC 서버 구성 server.listen({ key, cert, alpn: 'h3', // HTTP/3용 애플리케이션 계층 프로토콜 협상 maxConnections: 1000, requestCert: false, rejectUnauthorized: false }) .then(() => { console.log('HTTP/3 server listening on quic://localhost:8443'); }); server.on('session', (session) => { console.log(`QUIC 세션이 ${session.remoteAddress}:${session.remotePort}에서 설정되었습니다.`); session.on('stream', (stream) => { // HTTP/3 요청 헤더 읽기 stream.on('data', (data) => { // 실제 H3 구현에서는 ALPN 프레임과 HTTP/3 헤더를 파싱합니다. // 이것은 단순화된 예입니다. 실제 H3 파싱은 더 복잡합니다. console.log(`스트림 ${stream.id}에서 데이터 수신: ${data.toString()}`); }); stream.on('end', () => { // 간단한 응답의 경우 stream.write('Hello from Node.js HTTP/3!'); stream.end(); console.log(`스트림 ${stream.id} 종료됨`); }); stream.on('error', (err) => console.error(`스트림 오류: ${err.message}`); }); session.on('close', () => { console.log('QUIC 세션 종료됨'); }); session.on('error', (err) => console.error(`세션 오류: ${err.message}`); }); server.on('error', (err) => console.error(`QUIC 소켓 오류: ${err.message}`);
참고: HTTP/3 서버와 클라이언트 측에서 상호 작용하는 것은 더 복잡합니다. 오늘날 표준 브라우저는 특정 구성이나 프록싱 없이 사용자 지정 HTTP/3 서버에 쉽게 연결되지 않을 수 있습니다. curl(QUIC 지원 및 --http3 플래그 포함)과 같은 도구가 테스트에 더 적합합니다. 위의 예는 서버 설정을 보여주지만 스트림 data 이벤트에서 실제 HTTP/3 요청/응답 파싱에는 HTTP/3 프레임(SETTINGS, HEADERS, DATA 등)을 디코딩하는 작업이 포함되며, 이는 간단한 stream.write/end 호출 범위를 벗어납니다. 전체 HTTP/3 구현의 경우 node:quic 기반의 상위 수준 라이브러리를 사용하거나 Node.js가 직접 상위 수준 API를 제공하기로 결정하는 경우 http3 모듈 개발을 기다리는 것이 일반적입니다. 현재로서는 node:quic 모듈이 기반을 제공합니다.
애플리케이션 시나리오
이러한 프로토콜을 활용하면 다양한 유형의 애플리케이션을 크게 향상시킬 수 있습니다.
- 단일 페이지 애플리케이션(SPA): HTTP/2의 다중화 및 서버 푸시는 수많은 작은 애셋(JS, CSS, 이미지)을 가진 SPA에 이상적입니다. 중요한 애셋을 푸시하면 인식되는 로드 시간을 크게 줄일 수 있습니다.
- 실시간 대시보드/API: HTTP/2의 양방향 스트리밍은 일부 시나리오에서 WebSockets의 오버헤드 없이 실시간 업데이트에 유용합니다. 특히 요청/응답 의미론이 여전히 원하는 경우에 그렇습니다.
- 마이크로서비스 통신: 마이크로서비스 아키텍처 내에서 HTTP/2는 HTTP/1.1에 비해 서비스 간의 내부 통신을 더 효율적으로 제공할 수 있으며, 특히 장기 실행 연결을 통해 그렇습니다.
- 모바일 클라이언트: HTTP/3의 연결 마이그레이션 및 0-RTT/1-RTT 연결은 불안정한 네트워크 환경과 빈번한 연결 해제/재연결을 자주 경험하는 모바일 클라이언트에게 게임 체인저입니다. 감소된 연결 설정 시간은 특히 가치가 있습니다.
- 콘텐츠 전송 네트워크(CDN): CDN은 전역 콘텐츠 전송 속도와 신뢰성을 향상시키는 기능으로 인해 HTTP/3를 조기에 채택하고 있습니다. CDN과 상호 작용하거나 CDN의 일부를 구성하는 Node.js 애플리케이션은 이를 활용할 수 있습니다.
결론
Node.js의 HTTP/2 및 HTTP/3(QUIC)에 대한 네이티브 지원은 개발자에게 고성능, 복원력 있고 미래 보장적인 웹 서비스를 구축할 수 있는 강력한 도구를 제공합니다. 다중화, 서버 푸시, 헤더 압축 및 통합 TLS를 갖춘 QUIC의 UDP 기반 아키텍처의 기능을 이해하고 전략적으로 적용함으로써 개발자는 사용자 경험을 크게 향상시키고 지연 시간을 줄이며 리소스 활용도를 최적화할 수 있습니다. 이러한 최신 프로토콜을 채택하는 것은 오늘날의 빠르게 진행되는 디지털 환경에서 경쟁력을 유지하고 애플리케이션이 이전보다 더 빠르고 안정적으로 콘텐츠를 제공하도록 보장하는 데 필수적입니다. 웹의 미래는 더 빠르고 안전하며 Node.js는 그 길을 선도할 준비가 되어 있습니다.

