network
HTTP와 네트워크: HTTP/1.1 → HTTP/2 → HTTP/3, WebSocket, CDN, DNS
네트워크 지식은 프론트엔드 성능 최적화의 기반입니다. HTTP/2의 멀티플렉싱을 이해해야 번들링 전략을 결정할 수 있고, WebSocket과 SSE의 차이를 알아야 실시간 기능을 적절하게 구현할 수 있습니다. "URL 입력부터 화면 표시까지 무슨 일이 일어나는가"는 면접 클래식 질문입니다.
학습 개요
탄생 배경
해결하려 했던 문제
HTTP/1.1은 요청마다 연결을 맺거나 (Connection: keep-alive로 재사용해도) 응답을 순서대로 기다려야 했습니다. 웹 페이지가 수십 개의 리소스(JS, CSS, 이미지)를 로드하게 되면서 HOL(Head-of-Line) Blocking이 심각한 성능 문제가 되었습니다. HTTP/2는 이를 해결하고, HTTP/3은 TCP의 근본적인 한계를 해결하기 위해 등장했습니다.
역사적 맥락
1991년 HTTP/0.9 → 1996년 HTTP/1.0 → 1997년 HTTP/1.1 (keep-alive) → 2009년 Google SPDY 프로토콜(HTTP/2의 전신) → 2015년 HTTP/2 표준화 → 2018년 HTTP/3(QUIC) 초안 → 2022년 HTTP/3 RFC 9114 표준화 → 2023년 대부분의 주요 브라우저와 CDN이 HTTP/3 지원
이전에는 어떻게 했나
실시간 통신을 위한 Long Polling, Short Polling이 WebSocket 이전에 사용되었습니다. 서버 푸시를 위해 Comet 기법(숨겨진 iframe + keep-alive) 같은 해킹 방법도 있었습니다.
멘탈 모델
동작 원리
[HTTP/1.1의 한계] HOL Blocking: 한 연결에서 요청이 순서대로 처리되어야 함. 앞 요청의 응답을 받기 전에 다음 요청을 보낼 수 없음. 해결 시도: 도메인 샤딩 (여러 서브도메인으로 병렬 연결 수 늘리기), 번들링으로 요청 수 줄이기. 헤더 중복: 같은 헤더(User-Agent, Cookie 등)를 매 요청마다 반복 전송 (압축 없음). [HTTP/2 핵심 기능] 1. 멀티플렉싱 (Multiplexing): 하나의 TCP 연결에서 여러 요청/응답을 동시에, 순서 없이 처리. 각 요청은 Stream ID로 식별되어 뒤섞여도 클라이언트가 조립 가능. → 도메인 샤딩 불필요, 연결 수 최소화, HOL Blocking 해소 (TCP 레벨은 여전히 존재) 2. 헤더 압축 (HPACK): 이전에 전송한 헤더와 달라진 부분만 전송. 자주 사용하는 헤더는 정적 테이블 참조로 1바이트로 표현. 3. 서버 푸시 (Server Push): 클라이언트 요청 전에 서버가 필요한 리소스를 미리 전송. HTML 요청 시 CSS, JS를 미리 푸시. (현재는 Link: preload 헤더로 대체 중) 4. 스트림 우선순위: 중요한 리소스(CSS, JS)를 이미지보다 높은 우선순위로 처리. [HTTP/3와 QUIC] HTTP/2도 TCP 레벨 HOL Blocking은 해결 못함: 하나의 TCP 패킷이 손실되면 전체 연결이 대기 (TCP는 순서 보장 프로토콜). HTTP/3 = QUIC (UDP 기반) 위에서 동작: - UDP로 패킷 손실 시 해당 스트림만 재전송, 나머지는 계속 진행. - TLS 1.3이 QUIC에 통합되어 핸드셰이크가 1-RTT (또는 0-RTT 재연결). - Connection Migration: IP가 바뀌어도 (WiFi → LTE) 연결 유지. [HTTPS와 TLS 핸드셰이크] TLS 1.3 핸드셰이크 (1-RTT): 1. Client → Server: ClientHello (지원 암호화 방식, 랜덤값, 키 교환 파라미터) 2. Server → Client: ServerHello (선택된 암호화 방식, 인증서, 키 교환 완료) 3. 이후부터 암호화 통신 시작 인증서 검증: 브라우저 → 인증기관(CA)의 공개키로 서버 인증서의 서명 검증. [WebSocket vs HTTP Polling vs SSE] HTTP Polling (Short Polling): 클라이언트가 주기적으로 서버에 요청. 서버가 새 데이터 없으면 빈 응답. 단점: 불필요한 요청 과다, 실시간성 낮음. HTTP Long Polling: 서버가 응답을 보류하다가 새 데이터 있을 때 응답. 클라이언트는 즉시 재요청. 단점: 연결 유지 비용, 복잡한 서버 구현. WebSocket: 초기 HTTP 업그레이드 핸드셰이크 후 양방향 지속 연결. 헤더 오버헤드 없이 메시지 프레임 단위로 양방향 통신. 적합: 채팅, 실시간 게임, 협업 도구, 알림. SSE (Server-Sent Events): HTTP/1.1 위에서 서버 → 클라이언트 단방향 스트림. Content-Type: text/event-stream, 브라우저 내장 EventSource API. 자동 재연결 내장, HTTP/2와 함께 사용 시 다중 스트림 가능. 적합: 실시간 피드, 주가, 알림 (클라이언트 → 서버는 별도 HTTP 요청). [DNS 조회 과정 (URL 입력 후 벌어지는 일)] 1. URL 파싱: 프로토콜, 도메인, 경로 분리 2. DNS 조회: a. 브라우저 DNS 캐시 확인 b. OS DNS 캐시 확인 (/etc/hosts 포함) c. Router DNS 캐시 확인 d. ISP DNS Resolver에 질의 e. Root DNS Server → TLD(.com) DNS Server → Authoritative DNS Server f. IP 주소 반환 3. TCP 연결 (3-way handshake) + TLS 핸드셰이크 4. HTTP 요청 전송 5. 서버 응답 수신 6. 브라우저 렌더링 [CDN 동작 원리] CDN(Content Delivery Network): 전 세계 여러 서버(POP, Point of Presence)에 콘텐츠를 캐시. 사용자는 지리적으로 가장 가까운 서버에서 콘텐츠를 받음 → 레이턴시 감소. DNS를 통해 사용자 위치에 따라 가장 가까운 CDN 서버 IP를 반환. Cache-Control 헤더로 CDN 캐시 기간 설정. Edge Computing: CDN 서버에서 직접 로직 실행 (Cloudflare Workers, Vercel Edge Functions).
핵심 구성 요소
Multiplexing
HTTP/2의 핵심: 단일 연결에서 병렬 요청/응답 처리
QUIC
HTTP/3의 기반: UDP 위에 신뢰성, 암호화, 멀티플렉싱을 구현한 프로토콜
TLS Handshake
HTTPS 연결 수립 시 암호화 키를 안전하게 교환하는 과정
WebSocket
단일 TCP 연결의 양방향 전이중(Full-Duplex) 통신 채널
SSE
HTTP 기반 서버→클라이언트 단방향 이벤트 스트림
CDN
지리적으로 분산된 캐시 서버 네트워크. 레이턴시 감소와 오리진 서버 부하 감소
흐름 설명
[WebSocket 연결 수립]
1. 클라이언트 → 서버: HTTP Upgrade 요청
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
2. 서버 → 클라이언트: 101 Switching Protocols
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
3. 이후: HTTP 프로토콜이 아닌 WebSocket 프레임으로 통신
→ 헤더 없이 작은 프레임으로 양방향 실시간 통신
[SSE 연결]
1. 클라이언트: EventSource('https://api.example.com/events')
2. 브라우저 → 서버: GET /events HTTP/1.1 (Accept: text/event-stream)
3. 서버: HTTP 연결 유지, 데이터 있을 때마다 전송
data: {"type":"notification","message":"새 메시지"}
event: custom-event
data: {"payload":"..."}
4. 연결 끊어지면 브라우저 자동 재연결
코드 예제
// WebSocket 클라이언트
class WebSocketClient {
private ws: WebSocket;
private reconnectTimer: number | null = null;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = () => console.log('연결됨');
this.ws.onmessage = (event) => this.handleMessage(JSON.parse(event.data));
this.ws.onclose = () => this.reconnect(url);
this.ws.onerror = (error) => console.error('WebSocket 에러:', error);
}
send(data: object) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
private reconnect(url: string) {
this.reconnectTimer = window.setTimeout(() => this.connect(url), 3000);
}
}
// SSE 클라이언트 (단방향 서버 이벤트)
function useServerSentEvents(url: string) {
const [events, setEvents] = useState<Event[]>([]);
useEffect(() => {
const eventSource = new EventSource(url, {
withCredentials: true, // 쿠키 포함
});
eventSource.onmessage = (event) => {
setEvents(prev => [...prev, JSON.parse(event.data)]);
};
eventSource.addEventListener('notification', (event) => {
// 특정 이벤트 타입 처리
});
eventSource.onerror = () => {
// EventSource는 자동으로 재연결
};
return () => eventSource.close(); // 언마운트 시 연결 종료
}, [url]);
return events;
}
// HTTP/2에서 도메인 샤딩이 오히려 역효과인 이유
// HTTP/1.1: 브라우저 당 도메인별 6개 연결 → 도메인 샤딩으로 병렬 연결 증가
// HTTP/2: 단일 연결에서 멀티플렉싱 → 도메인 샤딩이 핸드셰이크 비용만 증가
// 따라서 HTTP/2 환경에서 도메인 샤딩은 오히려 성능 저하
// Cache-Control 헤더로 CDN 캐싱 전략
// 정적 자산: 긴 캐시 시간 + 파일명에 콘텐츠 해시
// bundle.a1b2c3d4.js
Cache-Control: public, max-age=31536000, immutable
// 변경 시 파일명이 달라지므로 캐시 무효화 자동
// HTML: 짧은 캐시
Cache-Control: public, max-age=0, must-revalidate
비교 분석
HTTP와
vsWebSocket vs SSE
HTTP와
vsHTTP/2 vs HTTP/1.1
트레이드오프
이상적인 사용 사례