
HTTP/2와 HTTP/3: 웹 속도 혁명
텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

웹사이트 성능 개선하다가 "HTTP/2 활성화하면 빨라집니다"라는 말을 봤습니다. "지금도 HTTP 쓰는데, HTTP/2가 뭐가 다르지?" 그냥 넘어가려다가, Chrome DevTools의 Network 탭을 열었습니다.
Performance 탭에서 Waterfall을 보는데, 파일 20개가 줄 서서 순차적으로 다운로드되고 있더라고요. 마치 은행 창구 하나만 열려 있고 20명이 줄 서 있는 느낌이었습니다. "왜 파일 20개가 줄 서서 다운로드되는 거야? 동시에 받으면 안 되나?" 이게 제 출발점이었습니다.
그때 처음으로 브라우저가 실제로 웹페이지를 어떻게 로딩하는지 시각적으로 봤고, 이 Waterfall 차트가 왜 폭포처럼 길게 늘어지는지 궁금했습니다. 나중에 알고 보니 이게 바로 HTTP/1.1의 근본적인 한계였습니다.
HTTP 버전 업그레이드가 이렇게 복잡할 줄 몰랐습니다.
처음엔 단순히 "최신 버전이니까 더 빠르겠지" 정도로만 생각했는데, 파고들수록 각 버전이 해결하려는 문제가 완전히 다르더라고요. 왜 바꿨는지 동기가 명확히 안 와닿았습니다. 그냥 "이론상 빠르다"는 설명만 봐서는 실감이 안 났습니다.
그러다가 이 비유를 듣고 단번에 이해했습니다:
"HTTP/1.1: 1차선 고속도로. 차(파일) 하나씩만 지나갈 수 있음. 앞 차가 느리면 뒤차도 다 느림. (Head-of-Line Blocking)
HTTP/2: 다차선 고속도로(Multiplexing). 한 도로(TCP 연결)에 여러 차선. HTML, CSS, JS가 동시에 달릴 수 있음.
HTTP/3: 터널(UDP) 고속도로. 공사 중이어도 다른 차선은 계속 달림. 패킷 하나 잃어버려도 전체가 멈추지 않음."
이게 핵심이었습니다! 결국 HTTP 진화의 역사는 "어떻게 하면 여러 파일을 더 빨리, 더 효율적으로 전송할까?"라는 문제를 푸는 과정이었던 거죠. 이 비유를 듣고 나니까 왜 HTTP/2가 Multiplexing을 도입했는지, 왜 HTTP/3가 UDP를 선택했는지가 명확하게 와닿았습니다.
HTTP/1.1의 가장 큰 문제는 한 번에 하나의 요청만 처리할 수 있다는 점입니다. 마치 은행 창구 하나만 열려 있는 상황과 같습니다.
[요청 순서]
1. index.html
2. style.css
3. script.js
4. image.png
[기존 HTTP/1.1]
index.html (2초) ━━━━━━
style.css (1초) ━━
script.js (1초) ━━
image.png (3초) ━━━━━━
총 7초
파일 하나씩 순차적으로만 다운로드됩니다. 앞 파일이 느리면 뒤 파일들이 다 기다려야 합니다. 이게 바로 Head-of-Line Blocking입니다. 맨 앞(Head)에 있는 요청이 느리면 뒤에 줄 선(Line) 모든 요청이 블로킹(Blocking)됩니다.
제가 처음 본 Chrome DevTools의 Waterfall이 길게 늘어진 이유가 바로 이것 때문이었습니다.
개발자들은 이 한계를 극복하기 위해 온갖 꼼수를 썼습니다. 지금 생각하면 웃긴데, 당시엔 이게 "Best Practice"였습니다.
브라우저는 보통 도메인당 6개 TCP 연결을 동시에 엽니다. 그래서 개발자들은 리소스를 여러 도메인으로 쪼갰습니다.
연결 1 (example.com): index.html
연결 2 (static1.example.com): style.css
연결 3 (static2.example.com): script.js
연결 4 (cdn1.example.com): image1.png
연결 5 (cdn2.example.com): image2.png
연결 6 (cdn3.example.com): image3.png
이렇게 하면 도메인이 3개니까 최대 18개 연결을 쓸 수 있었습니다. 하지만 이것도 비효율적입니다:
작은 아이콘 50개를 로딩하려면 50번의 HTTP 요청이 필요했습니다. 그래서 50개 아이콘을 하나의 큰 이미지로 합쳐서, CSS background-position으로 잘라 쓰는 방식이 유행했습니다.
.icon-home {
background: url('sprites.png') 0 0;
}
.icon-user {
background: url('sprites.png') -20px 0;
}
유지보수 지옥이었지만, 어쩔 수 없었습니다.
CSS 파일 10개, JS 파일 15개를 각각 하나씩 합쳐서 bundle.css, bundle.js 두 파일로 만들었습니다. Webpack, Rollup 같은 번들러가 등장한 이유 중 하나가 바로 이겁니다.
문제는 파일 하나만 수정해도 전체 번들을 다시 다운로드해야 했습니다. 캐싱 효율이 최악이었죠.
이 모든 Workaround가 HTTP/2 이후엔 불필요해졌습니다. 지금 이해했다: 예전 개발 방식들이 왜 저렇게 복잡했는지. 프로토콜의 한계를 애플리케이션 레벨에서 우회하려다 보니 복잡도가 폭발했던 겁니다.
2015년에 HTTP/2가 등장했습니다. Google의 SPDY 프로토콜을 기반으로 만들어졌고, 핵심 목표는 "한 연결로 모든 걸 해결하자"였습니다.
HTTP/2의 가장 큰 혁신입니다. 하나의 TCP 연결에서 여러 요청과 응답을 동시에 처리할 수 있습니다.
[HTTP/2 Multiplexing]
하나의 TCP 연결에서:
index.html ━━━━━━
style.css ━━
script.js ━━
image.png ━━━━━━
총 3초 (가장 긴 파일 기준)
어떻게 가능한가? Stream과 Frame이라는 개념을 도입했습니다.
HTTP/2는 데이터를 작은 조각(Frame)으로 쪼개서 보냅니다. 각 요청/응답은 하나의 Stream으로 관리됩니다.
[하나의 TCP 연결]
┌─────────────────────────────────────┐
│ Frame 1 (Stream 1: index.html) │
│ Frame 2 (Stream 2: style.css) │
│ Frame 3 (Stream 1: index.html) │
│ Frame 4 (Stream 3: script.js) │
│ Frame 5 (Stream 2: style.css) │
│ Frame 6 (Stream 1: index.html) │
└─────────────────────────────────────┘
서버는 Frame을 번갈아가며 보내고, 클라이언트는 Stream ID를 보고 조합합니다. 마치 편지를 찢어서 보내는데 각 조각에 번호가 적혀 있어서 나중에 맞출 수 있는 것과 같습니다.
모든 Stream이 똑같이 중요한 건 아닙니다. HTML은 빨리 로딩해야 하고, 배너 광고 이미지는 나중에 와도 됩니다.
HTTP/2는 Stream에 우선순위를 부여할 수 있습니다:
[Stream Priority]
Stream 1 (index.html) : Priority 256 (highest)
Stream 2 (style.css) : Priority 220
Stream 3 (script.js) : Priority 220
Stream 4 (ad-banner.jpg) : Priority 2 (lowest)
브라우저가 우선순위를 설정하면, 서버는 중요한 리소스부터 먼저 보냅니다.
Stream마다 독립적인 flow control이 있습니다. 느린 클라이언트가 한 Stream을 처리하는 동안, 다른 Stream은 계속 데이터를 받을 수 있습니다.
Stream 1: [Window Size: 65535 bytes] ✓ 받을 준비 됨
Stream 2: [Window Size: 0 bytes] ✗ 아직 처리 중
Stream 3: [Window Size: 32768 bytes] ✓ 절반 준비 됨
이 모든 게 하나의 TCP 연결에서 일어납니다. 이제 Domain Sharding이 불필요해진 이유를 이해했습니다.
HTTP/1.1은 텍스트 기반 프로토콜입니다:
GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, deflate, br
사람이 읽기 편하지만, 컴퓨터가 파싱하기엔 비효율적입니다. 줄바꿈(\r\n)을 찾아야 하고, 헤더 이름과 값을 분리해야 하고, 대소문자 구분도 해야 합니다.
HTTP/2는 바이너리로 전환했습니다:
[HTTP/2 Binary Frame]
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
처음엔 "사람이 못 읽으면 디버깅 어렵지 않나?" 걱정했는데, Chrome DevTools가 알아서 해석해주니까 문제없더라고요.
Server Push는 이론상 굉장히 매력적인 기능이었습니다.
[클라이언트]
GET /index.html
[서버]
여기 index.html
(아, 이 페이지엔 style.css도 필요하겠네?)
style.css도 같이 줄게! (미리 푸시)
클라이언트가 요청하기 전에 서버가 미리 보내줍니다. RTT를 절약할 수 있는 아름다운 아이디어였습니다.
하지만 현실은 달랐습니다. Chrome은 2022년에 Server Push 지원을 완전히 제거했습니다. 왜일까요?
캐시 문제: 서버는 클라이언트의 캐시 상태를 모릅니다. 이미 style.css가 캐시에 있는데도 또 보내면 낭비입니다.
우선순위 충돌: 서버가 "중요하다고 생각해서" 보낸 리소스가 클라이언트 입장에선 낮은 우선순위일 수 있습니다.
복잡도: 어떤 리소스를 푸시할지 결정하는 로직이 복잡합니다. 잘못하면 오히려 성능 저하.
103 Early Hints가 더 나음: 서버가 푸시하는 대신, "이런 리소스가 필요할 거야"라고 힌트만 주면 브라우저가 알아서 요청합니다. 캐시 확인도 하고, 우선순위도 브라우저가 제어합니다.
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
HTTP/1.1 200 OK
Content-Type: text/html
...
받아들였습니다: 좋은 아이디어가 항상 현실에서 성공하는 건 아니라는 것을. Server Push는 HTTP/2의 실패한 실험이었지만, 그 과정에서 더 나은 대안(Early Hints)을 찾았습니다.
HTTP 헤더는 매 요청마다 반복됩니다. 특히 쿠키가 크면 헤더만 수 KB씩 됩니다.
GET /page1.html
Host: example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123; user_id=456; preferences=...
Accept-Encoding: gzip, deflate, br
GET /page2.html
Host: example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123; user_id=456; preferences=...
Accept-Encoding: gzip, deflate, br
거의 똑같은 헤더를 반복해서 보냅니다.
HPACK은 Static Table + Dynamic Table로 헤더를 압축합니다.
Static Table: 자주 쓰는 헤더를 미리 정의해둔 표
Index | Header Name | Header Value
------|-------------------|-------------
1 | :authority |
2 | :method | GET
3 | :method | POST
4 | :path | /
...
15 | accept-encoding | gzip, deflate
...
Dynamic Table: 연결 중에 나온 헤더를 저장하는 표
첫 요청:
Host: example.com → Dynamic Table에 저장 (Index 62)
User-Agent: Mozilla/5.0... → Dynamic Table에 저장 (Index 63)
두 번째 요청:
Host: example.com → "Index 62 사용" (2바이트면 끝)
User-Agent: Mozilla/5.0... → "Index 63 사용"
실제 압축 효과:
[압축 전]
GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...
Cookie: sessionId=abc123def456; userId=789; preferences=dark_mode,lang_ko
Accept: application/json
Accept-Encoding: gzip, deflate, br
→ 약 250바이트
[HPACK 압축 후]
82 86 84 41 8c f1 e3 c2 e5 f2 3a 6b a0 ab 90 f4 ff
→ 약 30바이트 (88% 감소!)
이제 이해했다: HTTP/2가 왜 모바일 환경에서 특히 효과적인지. 헤더 압축으로 대역폭을 크게 절약하니까요.
HTTP/2가 완벽해 보였는데, 왜 HTTP/3가 나왔을까요? HTTP/2에도 해결 못한 근본적인 문제가 있었습니다. 바로 TCP 자체의 한계.
HTTP/2는 애플리케이션 레벨에서 Multiplexing을 하지만, 여전히 TCP 위에서 동작합니다. TCP는 패킷 순서를 보장하기 때문에, 패킷 하나가 손실되면 모든 Stream이 멈춥니다.
[HTTP/2 over TCP]
Stream 1: HTML ━━━━━━
Stream 2: CSS ━━
Stream 3: JS ━━
Stream 4: Image ━━━━━━
[TCP 패킷 레벨]
Packet 1: HTML chunk 1 ✓
Packet 2: CSS chunk 1 ✓
Packet 3: HTML chunk 2 ✗ 손실!
Packet 4: JS chunk 1 ✓ (도착했지만 대기)
Packet 5: Image chunk 1 ✓ (도착했지만 대기)
→ Packet 3 재전송 완료할 때까지 Packet 4, 5도 블록됨
HTTP/2는 Stream을 분리했지만, TCP는 전체를 하나로 봅니다. Stream 1의 패킷이 손실되면 Stream 2, 3, 4도 다 기다립니다. 이게 TCP의 Head-of-Line Blocking입니다.
TCP + TLS 연결 설정:
[TCP 3-way Handshake]
클라이언트 → 서버: SYN
서버 → 클라이언트: SYN-ACK
클라이언트 → 서버: ACK
→ 1.5 RTT
[TLS 1.2 Handshake]
클라이언트 → 서버: ClientHello
서버 → 클라이언트: ServerHello, Certificate
클라이언트 → 서버: KeyExchange, Finished
→ 1.5 RTT
총 3 RTT (약 300ms @ 100ms latency)
페이지 하나 보려고 300ms를 기다려야 합니다. 모바일 네트워크에선 latency가 더 크니까 500ms 이상 걸리기도 합니다.
TCP는 연결을 (소스 IP, 소스 포트, 목적지 IP, 목적지 포트) 4개 값으로 식별합니다. IP가 바뀌면 연결이 끊깁니다.
[상황: 지하철에서 유튜브 시청]
WiFi (192.168.1.100) → 동영상 스트리밍 중
↓ 터널 진입, WiFi 끊김
LTE (10.20.30.40) → IP 변경!
→ TCP 연결 끊김 → 재연결 → 버퍼링...
실제로 지하철 타면서 유튜브 보면 터널 들어갈 때마다 끊기는 이유입니다.
Google은 과감하게 UDP를 선택했습니다. "UDP는 신뢰성이 없는데?" 맞습니다. 그래서 UDP 위에 신뢰성을 직접 구현했습니다.
OS 커널 수정 불필요: TCP는 OS 커널에 구현되어 있어서 수정하려면 OS 업데이트가 필요합니다. UDP는 애플리케이션 레벨에서 구현 가능합니다.
중간 장비 간섭 없음: 방화벽, NAT 같은 중간 장비들이 TCP 패킷을 분석하고 수정합니다. UDP는 단순해서 간섭이 적습니다.
빠른 진화 가능: 프로토콜 개선이 필요하면 애플리케이션 업데이트만 하면 됩니다.
UDP는 거의 아무것도 안 합니다. 패킷 전송만 할 뿐, 손실 복구, 순서 보장, 혼잡 제어를 하지 않습니다. QUIC은 이 모든 걸 직접 구현했습니다.
[QUIC Stack]
┌─────────────────────────────────┐
│ HTTP/3 (애플리케이션 레이어) │
├─────────────────────────────────┤
│ QUIC (전송 레이어) │
│ - 신뢰성 보장 (재전송) │
│ - 순서 보장 (Stream별) │
│ - 혼잡 제어 │
│ - 암호화 (TLS 1.3 내장) │
├─────────────────────────────────┤
│ UDP (단순 패킷 전송) │
└─────────────────────────────────┘
QUIC은 TLS 1.3을 내장하고, 0-RTT 연결을 지원합니다.
[첫 연결 - 1-RTT]
클라이언트 → 서버: ClientHello (암호화 협상)
서버 → 클라이언트: ServerHello + 암호화된 응답
→ 1 RTT
[재연결 - 0-RTT]
클라이언트 → 서버: 이전 세션 티켓 + 암호화된 HTTP 요청
서버 → 클라이언트: 암호화된 HTTP 응답
→ 0 RTT! 첫 패킷에 데이터 포함
재연결 시 즉시 요청을 보낼 수 있습니다. RTT를 완전히 제거한 겁니다.
하지만 0-RTT에는 치명적인 보안 문제가 있습니다. Replay Attack입니다.
[Replay Attack 시나리오]
1. Alice → 서버: "계좌 A에서 B로 $100 송금" (0-RTT)
2. 공격자가 이 패킷을 복사
3. 공격자 → 서버: (같은 패킷 재전송)
4. 서버: "OK, $100 송금 완료" (또!)
→ $200 송금됨
0-RTT 패킷은 암호화되어 있지만, 공격자가 내용을 모른 채로도 재전송할 수 있습니다.
QUIC은 여러 방어 메커니즘을 씁니다:
// Node.js에서 0-RTT 허용 범위 설정
const http3Server = require('http3');
http3Server.createServer({
allowEarlyData: true,
maxEarlyData: 16384, // 0-RTT로 받을 최대 바이트
earlyDataCallback: (req) => {
// GET, HEAD만 0-RTT 허용
if (req.method !== 'GET' && req.method !== 'HEAD') {
return false;
}
return true;
}
});
정리해본다: 0-RTT는 성능과 보안의 트레이드오프입니다. 안전한 요청(멱등성)만 0-RTT로 보내고, 중요한 요청(송금, 결제)는 1-RTT로 보내는 게 합리적입니다.
QUIC은 TCP와 달리 Stream별로 독립적인 순서 보장을 합니다.
[QUIC Stream Independence]
Stream 1: HTML ━━━━━━ ✓
Stream 2: CSS ━━ ✓
Stream 3: JS ✗ 패킷 손실! → 재전송 중
Stream 4: Image ━━━━━━ ✓
→ Stream 3만 멈춤, Stream 1, 2, 4는 계속 진행
Stream 3의 패킷 손실이 다른 Stream에 영향을 주지 않습니다. TCP의 Head-of-Line Blocking을 완전히 해결했습니다.
QUIC은 연결을 Connection ID로 식별합니다. IP나 포트가 바뀌어도 Connection ID가 같으면 연결이 유지됩니다.
[QUIC Connection Migration]
WiFi (IP: 192.168.1.100, Connection ID: 0x1a2b3c4d)
→ Stream 1: 동영상 다운로드 중...
[WiFi 끊김, LTE로 전환]
LTE (IP: 10.20.30.40, Connection ID: 0x1a2b3c4d)
→ 같은 Connection ID
→ Stream 1: 계속 다운로드 (끊김 없음!)
실제 시나리오: 지하철에서 스마트폰으로 유튜브 시청
[기존 HTTP/2 over TCP]
역 안 (WiFi) → 동영상 스트리밍
터널 진입 → WiFi 끊김 → TCP 연결 끊김
터널 내 (LTE) → 재연결 (3 RTT) → 버퍼링 3초
→ 사용자: "아 왜 끊겨!"
[HTTP/3 over QUIC]
역 안 (WiFi) → 동영상 스트리밍
터널 진입 → WiFi 끊김 → Connection ID 유지
터널 내 (LTE) → 즉시 재개
→ 사용자: "어? 안 끊기네?"
이거 받아들였을 때 진짜 놀랐습니다. "네트워크 전환에도 연결 유지"라는 게 가능하다니. 모바일 시대에 딱 맞는 프로토콜이었습니다.
먼저 내 사이트가 어떤 HTTP 버전을 쓰는지 직접 확인해봤다.
Name Status Type Protocol
index.html 200 document h2
style.css 200 stylesheet h2
script.js 200 script h2
image.png 200 png h2
h2는 HTTP/2, h3는 HTTP/3입니다.
# HTTP/2 테스트
curl -I --http2 https://example.com
# HTTP/3 테스트 (curl 7.72.0+)
curl -I --http3 https://example.com
# 자세한 정보 보기
curl -I --http2 -v https://example.com 2>&1 | grep "ALPN"
# ALPN: server accepted h2 → HTTP/2
# ALPN: server accepted h3 → HTTP/3
제 사이트는 Nginx를 씁니다. HTTP/2 활성화는 한 줄이면 됩니다.
server {
listen 443 ssl http2; # http2 활성화
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# HTTP/2 Server Push (선택사항, 비추천)
# http2_push /style.css;
# http2_push /script.js;
location / {
root /var/www/html;
index index.html;
}
}
주의: http2_push는 위에서 설명했듯이 실패한 기능입니다. 쓰지 마세요.
설정 후 재시작:
sudo nginx -t # 설정 검증
sudo systemctl reload nginx
Nginx 1.25.0부터 HTTP/3를 실험적으로 지원합니다. 컴파일 옵션에 --with-http_v3_module이 필요합니다.
server {
listen 443 ssl http2;
listen 443 quic reuseport; # HTTP/3 활성화
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# HTTP/3 지원 알림 (Alt-Svc 헤더)
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
root /var/www/html;
index index.html;
}
}
Alt-Svc 헤더는 "이 서버는 HTTP/3도 지원해요"라고 브라우저에게 알립니다. 브라우저는 다음 요청부터 HTTP/3를 시도합니다.
제 사이트는 Cloudflare를 씁니다. Cloudflare는 자동으로 HTTP/3를 지원합니다. 별도 설정 불필요!
Cloudflare 대시보드:
# Cloudflare가 HTTP/3 지원하는지 확인
curl -I --http3 https://codemapo.com
# HTTP/3 200 ✓
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('/path/to/key.pem'),
cert: fs.readFileSync('/path/to/cert.pem')
});
server.on('stream', (stream, headers) => {
const path = headers[':path'];
console.log(`Stream ID ${stream.id}: ${path}`);
if (path === '/') {
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end('<h1>HTTP/2 Server</h1>');
} else if (path === '/data') {
stream.respond({
'content-type': 'application/json',
':status': 200
});
stream.end(JSON.stringify({ message: 'Multiplexing works!' }));
}
});
server.listen(8443, () => {
console.log('HTTP/2 server running on https://localhost:8443');
});
브라우저에서 https://localhost:8443을 열면 HTTP/2로 서빙됩니다. DevTools에서 h2 확인 가능합니다.
제가 실제로 측정한 결과입니다 (Next.js 사이트, 파일 20개, Cloudflare CDN).
| 프로토콜 | 로딩 시간 | 비고 |
|---|---|---|
| HTTP/1.1 | 2.8초 | 파일 20개 순차 로딩 (6개 연결) |
| HTTP/2 | 1.2초 | Multiplexing 효과 (1개 연결) |
| HTTP/3 | 1.0초 | 0-RTT 재연결, Stream 독립성 |
HTTP/1.1: 계단식 (순차 로딩)
index.html ━━━━━━
style1.css ━━
style2.css ━━
script.js ━━━━
image1.png ━━━
HTTP/2: 병렬 로딩
index.html ━━━━━━
style1.css ━━
style2.css ━━
script.js ━━━━
image1.png ━━━
image2.png ━━━
...
HTTP/3: 병렬 + 빠른 재연결
(캐시에서 재방문)
0-RTT 연결 (즉시) → 모든 파일 병렬 로딩
이 차이가 와닿았습니다. 숫자만 봐선 몰랐는데, Waterfall 차트로 보니까 명확하더라고요.
HTTP 진화의 핵심을 정리해본다:
문제: Head-of-Line Blocking, 파일마다 TCP 연결 필요 해결: Multiplexing (한 연결로 모든 파일), Binary Framing (효율적 파싱), HPACK (헤더 압축) 효과: 성능 2~3배 향상, Domain Sharding/Sprite Sheet 불필요
문제: TCP의 Head-of-Line Blocking, 느린 연결 설정, Connection Migration 불가 해결: QUIC (UDP 기반), Stream 독립성, 0-RTT 연결, Connection ID 효과: 모바일 환경에서 특히 빠름, 네트워크 전환 시 끊김 없음
브라우저와 CDN이 알아서 최적의 프로토콜을 선택합니다. 우리는 그냥 쓰면 됩니다.
하지만 개발자라면 알아야 합니다:
처음엔 "HTTP면 다 똑같은 거 아닌가?" 싶었지만, 지금은 웹 성능 최적화할 때 HTTP/2와 HTTP/3 지원 여부를 필수로 확인합니다. 결국 이거였다: 프로토콜의 진화는 단순히 "빠른 것"만이 아니라, 웹 개발 방식 자체를 바꿨습니다. 더 이상 파일을 억지로 합치거나 도메인을 쪼갤 필요가 없습니다. 프로토콜이 똑똑해지니까 개발이 단순해졌습니다.