[HTTP] SSE, HTTP 정리
SSE의 HTTP 메서드
SSE는 GET 메서드만 사용함.
이유:
- 서버→클라이언트 단방향 통신이므로 GET으로 충분
- 처음 한 번만 요청하면 연결이 계속 유지됨
javascript
// 클라이언트: GET 요청 1회로 연결 시작
const eventSource = new EventSource('/events');
// 서버가 계속 데이터 푸시
eventSource.onmessage = (event) => {
console.log(event.data);
};HTTP 버전과 Keep-Alive
HTTP/1.0
- Keep-alive 기본값: OFF
- 매 요청마다 새 TCP 연결 생성
- 사용하려면 Connection: keep-alive 헤더 필요
HTTP/1.1
- Keep-alive 기본값: ON
- 하나의 TCP 연결로 여러 요청/응답 재사용
- SSE는 이 특성을 활용해 긴 연결 유지
HTTP/2
- 멀티플렉싱으로 더 효율적
- 여러 SSE 스트림 동시 처리 가능
- 하지만 SSE에 필수는 아님
TCP 연결 = 소켓 쌍
하나의 TCP 연결 구성
text
클라이언트: 192.168.1.100:54321
↕ (이 4개 값이 하나의 소켓 쌍)
서버: 93.184.216.34:443SSE 동작
text
같은 소켓 쌍 유지:
GET /events →
← data: message1
← data: message2
← data: message3
... (연결 계속 유지)왜 클라이언트가 먼저 요청해야 하나?
1. 방화벽/NAT 때문
클라이언트 선빵 시:
text
1. 클라이언트 → 서버: 연결 요청
2. 방화벽: "이 세션 추적 시작"
3. 서버 → 클라이언트: 응답 및 지속적 데이터 전송
4. 방화벽: "허용된 세션이니 통과!"서버 선빵 시:
text
1. 서버 → 클라이언트: 연결 시도
2. 방화벽: "요청하지 않은 연결, 차단!"2. HTTP 프로토콜 규칙
HTTP의 근본 원칙:
- 요청-응답(Request-Response) 프로토콜
- 클라이언트가 반드시 먼저 요청
- 서버는 응답만 가능
- RFC 스펙에 명시된 설계 철학
모든 HTTP 버전 공통:
- HTTP/1.1: 클라이언트 요청 → 서버 응답
- HTTP/2: 클라이언트 요청 → 서버 응답
- HTTP/3: 클라이언트 요청 → 서버 응답
SSE 연결 흐름 상세
초기 연결 과정
text
클라이언트 서버
| |
|--- GET /events ---------------->|
| Connection: keep-alive |
| Accept: text/event-stream |
| |
|<-- 200 OK ---------------------|
| Content-Type: text/event-stream
| Cache-Control: no-cache |
| Connection: keep-alive |
| |
|<-- data: message1 -------------|
|<-- data: message2 -------------|
|<-- data: message3 -------------|
| ... (연결 유지) |핵심 헤더
http
# 클라이언트 요청
GET /events HTTP/1.1
Accept: text/event-stream
Connection: keep-alive
# 서버 응답
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-aliveSSE vs 일반 HTTP 요청
일반 HTTP 요청
text
클라이언트 서버
| |
|--- GET /data ------------------>|
|<-- 200 OK + 데이터 ------------|
| |
연결 종료 (또는 풀로 반환)SSE 요청
text
클라이언트 서버
| |
|--- GET /events ---------------->|
|<-- 200 OK ---------------------|
|<-- data: msg1 -----------------|
|<-- data: msg2 -----------------|
|<-- data: msg3 -----------------|
| ... (계속 유지) |차이점:
- 일반 요청: 응답 후 연결 종료 또는 재사용 대기
- SSE: 응답 헤더 후에도 연결 유지, 계속 데이터 전송
왜 서버가 먼저 연결할 수 없나?
기술적 이유
1. TCP 연결 시작 불가
python
# 서버 입장
# ❌ 불가능: 클라이언트 IP/포트를 모름
socket.connect(('어디?', '몇 번 포트?'))
# ✅ 가능: 특정 포트에서 대기
socket.bind(('0.0.0.0', 80))
socket.listen()2. 클라이언트의 동적 포트
text
클라이언트가 사용하는 포트는 매번 다름:
- 첫 번째 연결: 192.168.1.100:54321
- 두 번째 연결: 192.168.1.100:54322
- 세 번째 연결: 192.168.1.100:54323
서버는 클라이언트가 다음에 어떤 포트를 쓸지 알 수 없음3. 방화벽/NAT 통과 불가
text
가정/회사 네트워크:
[클라이언트] ---> [NAT/방화벽] ---> [인터넷] ---> [서버]
사설 IP 공인 IP 변환
클라이언트 → 서버: 가능 (NAT가 매핑 생성)
서버 → 클라이언트: 불가능 (매핑 없음, 차단됨)설계적 이유
HTTP는 클라이언트-서버 모델
text
클라이언트 역할:
- 연결 시작
- 요청 전송
- 서비스 소비자
서버 역할:
- 연결 대기
- 응답 전송
- 서비스 제공자SSE 실전 예제
서버 구현 (FastAPI)
python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
app = FastAPI()
async def event_generator():
"""SSE 이벤트 생성기"""
count = 0
while True:
count += 1
# SSE 형식: "data: 메시지\n\n"
yield f"data: Message {count}\n\n"
await asyncio.sleep(1)
@app.get("/events")
async def sse_endpoint():
"""SSE 엔드포인트"""
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)클라이언트 구현 (JavaScript)
javascript
// SSE 연결 생성
const eventSource = new EventSource('/events');
// 메시지 수신
eventSource.onmessage = (event) => {
console.log('받은 데이터:', event.data);
};
// 연결 열림
eventSource.onopen = () => {
console.log('SSE 연결 성공');
};
// 에러 처리
eventSource.onerror = (error) => {
console.error('SSE 에러:', error);
eventSource.close();
};
// 연결 종료
// eventSource.close();클라이언트 구현 (Python)
python
import httpx
import asyncio
async def consume_sse():
"""SSE 스트림 소비"""
async with httpx.AsyncClient() as client:
async with client.stream('GET', 'http://localhost:8000/events') as response:
async for line in response.aiter_lines():
if line.startswith('data: '):
data = line[6:] # "data: " 제거
print(f'받은 데이터: {data}')
# 실행
asyncio.run(consume_sse())SSE 데이터 형식
기본 형식
text
data: 메시지 내용\n\n여러 줄 데이터
text
data: 첫 번째 줄
data: 두 번째 줄
data: 세 번째 줄
이벤트 타입 지정
text
event: userConnected
data: {"userId": 123}
event: messageReceived
data: {"text": "안녕하세요"}
ID 지정 (재연결 지원)
text
id: 1
data: 첫 번째 메시지
id: 2
data: 두 번째 메시지
재연결 시간 설정
text
retry: 3000
data: 3초 후 재연결
SSE의 제약사항
1. 단방향 통신
text
클라이언트 → 서버: ❌ (초기 연결만 가능)
서버 → 클라이언트: ✅ (계속 가능)해결책:
- 양방향 필요 시 WebSocket 사용
- 또는 SSE + 일반 HTTP 요청 조합
2. 브라우저 연결 제한
text
같은 도메인에 대한 SSE 연결 제한:
- 대부분 브라우저: 6개
- 초과 시 새 연결 대기해결책:
- HTTP/2 사용 (제한 완화)
- 여러 도메인 사용
- 하나의 SSE로 여러 이벤트 처리
3. 텍스트만 전송 가능
text
✅ 가능: JSON, XML, 일반 텍스트
❌ 불가능: 바이너리 데이터 직접 전송해결책:
- Base64 인코딩
- WebSocket 사용
SSE vs WebSocket vs Long Polling
| 특성 | SSE | WebSocket | Long Polling |
|---|---|---|---|
| 프로토콜 | HTTP | WebSocket (ws://) | HTTP |
| 방향 | 서버→클라이언트 | 양방향 | 양방향 (연속 요청) |
| 연결 | 지속적 | 지속적 | 일시적 (반복) |
| 복잡도 | 낮음 | 중간 | 높음 |
| 브라우저 지원 | 대부분 (IE 제외) | 모든 최신 브라우저 | 모든 브라우저 |
| 재연결 | 자동 | 수동 구현 필요 | 자동 (새 요청) |
| 오버헤드 | 낮음 | 낮음 | 높음 (반복 요청) |
언제 SSE를 사용하나?
✅ SSE가 적합한 경우
- 서버에서 클라이언트로만 데이터 전송
- 실시간 알림, 피드 업데이트
- 주식 시세, 스포츠 스코어
- 로그 스트리밍, 모니터링
- 간단한 구현 필요
python
# 실시간 알림 예시
async def notification_stream(user_id: int):
while True:
notification = await get_user_notification(user_id)
if notification:
yield f"data: {notification.json()}\n\n"
await asyncio.sleep(5)❌ SSE가 부적합한 경우
- 양방향 통신 필요
- 바이너리 데이터 전송
- 매우 낮은 지연시간 필요
- 복잡한 프로토콜 필요
python
# 이런 경우는 WebSocket 사용
# - 채팅 애플리케이션
# - 멀티플레이어 게임
# - 협업 도구 (실시간 문서 편집)
# - 화상 통화종합 정리
핵심 개념
| 개념 | 설명 |
|---|---|
| SSE 메서드 | GET만 사용 (단방향 통신) |
| Keep-Alive | HTTP/1.1 기본 ON, 긴 연결 유지 |
| 소켓 쌍 | 4-tuple로 식별되는 단일 TCP 연결 |
| 클라이언트 먼저 | HTTP 프로토콜 규칙, 방화벽 통과 |
| 연결 유지 | 응답 후에도 계속 데이터 전송 |
| 데이터 형식 | data: 내용\n\n 텍스트 기반 |
SSE 체크리스트
✅ 구현 시 확인사항:
- Content-Type: text/event-stream 설정
- Cache-Control: no-cache 설정
- Connection: keep-alive 유지
- 데이터 형식: data: 내용\n\n
- 재연결 로직 구현 (자동 또는 수동)
- 에러 처리
✅ 최적화:
- 적절한 retry 시간 설정
- id 필드로 중복 방지
- 연결 수 제한 고려
- HTTP/2 사용 검토
❌ 피해야 할 것:
- 양방향 통신이 필요한데 SSE 사용
- 바이너리 데이터 직접 전송
- 재연결 로직 없이 구현
- 과도한 동시 연결
댓글을 불러오는 중...