소개
이 프로젝트는 SSE (Server-Sent Events) 와 Redis 를 활용하여 온라인 채팅방을 구현합니다. Python 및 JavaScript 구문에 대한 사전 지식과 Flask 및 Redis 사용에 대한 기본적인 이해가 필요합니다.
이 실험 섹션에서는 다음 개념들을 배우고 실습할 것입니다:
- 웹 실시간 통신
- SSE 의 작동 방식
- Redis 활용
👀 미리보기

🎯 과제
이 프로젝트에서 다음을 배우게 됩니다:
- Flask 와 SSE 를 사용하여 간단한 온라인 채팅방을 만드는 방법
- 사용자 로그인 기능을 구현하는 방법
- 메시지 저장 및 검색을 위해 Redis 를 사용하는 방법
🏆 성과
이 프로젝트를 완료하면 다음을 수행할 수 있습니다:
- 웹 애플리케이션에서 실시간 통신을 위해 SSE 를 설정
- 채팅방 애플리케이션에서 메시지를 저장하고 검색하기 위해 Redis 를 사용
- Flask 에서 사용자 로그인 기능 구현
웹 실시간 통신
웹 실시간 통신 (Web Real-Time Communication, WebRTC) 은 사용자가 페이지를 새로 고칠 필요 없이 웹 페이지에서 이벤트에 대해 즉시 알릴 수 있는 메커니즘을 의미합니다. WebRTC 는 실시간 채팅 및 인스턴트 메시징과 같은 다양한 용도로 사용됩니다.
웹 클라이언트와 서버 간의 통신에는 여러 가지 방법이 있습니다.
일반 HTTP 흐름
- 클라이언트가 서버에 웹 페이지를 요청합니다.
- 서버가 이에 따라 응답합니다.
- 서버가 응답을 클라이언트에 다시 보냅니다.

HTTP 요청은 상태 비저장 (stateless) 이므로, 각 요청 후에 HTTP 연결이 종료됩니다. 따라서 서버와 브라우저는 관련 정보를 업데이트하기 위해 다음 요청이 이루어질 때까지 서로를 완전히 인식하지 못합니다. 이 시점에서 브라우저가 주기적으로 요청을 보내 실시간 효과를 시뮬레이션할 수 있는 간단한 솔루션을 생각하는 것은 어렵지 않습니다. 이를 폴링 (polling) 이라고 합니다.
폴링 흐름
- 클라이언트가 일반 HTTP 를 사용하여 서버에 연결 요청을 보냅니다.
- 클라이언트는 웹 페이지에 포함된 JavaScript 폴링 스크립트를 실행하여 정보를 검색하기 위해 서버에 주기적으로 요청을 보냅니다 (예: 5 초마다).
- 서버는 각 요청에 응답하고 일반 HTTP 요청과 마찬가지로 해당 정보를 다시 보냅니다.

폴링을 사용하면 거의 실시간으로 정보를 얻을 수 있습니다. 그러나 폴링으로 인해 브라우저에서 서버로의 빈번한 요청은 성능 비효율성을 초래할 수 있습니다. 이러한 문제를 완화하기 위해 대안적인 방법이 제안되었습니다. 클라이언트 요청에 즉시 응답하는 대신, 서버는 데이터 변경 (또는 시간 초과) 이 발생할 때까지 응답을 반환하지 않습니다. 이 접근 방식은 폴링에서 요청 수를 줄이기 위해 연결 유효성을 최대화합니다. 이 방법을 롱 폴링 (long polling) 이라고 합니다.
롱 폴링 흐름
- 클라이언트가 일반 HTTP 를 사용하여 서버에 웹 페이지를 요청합니다.
- 클라이언트는 웹 페이지에 포함된 JavaScript 스크립트를 실행하여 데이터를 보내고 서버에 정보를 요청합니다.
- 서버는 클라이언트의 요청에 즉시 응답하는 대신 유효한 업데이트를 기다립니다.
- 정보가 업데이트되고 유효하면 서버는 데이터를 클라이언트에 푸시합니다.
- 서버의 알림을 받으면 클라이언트는 즉시 새 요청을 보내 다음 폴링 라운드를 시작합니다.

위에 언급된 방법은 실시간 웹 통신을 구현하는 데 일반적으로 사용됩니다. 그러나 HTML5 가 도입된 후 더 나은 옵션을 사용할 수 있게 되었습니다. HTML5 에서는 Server-Sent Events (SSE) 또는 WebSocket 을 사용할 수 있습니다. SSE 는 서버에서 클라이언트로 데이터를 푸시하도록 특별히 설계되었으며, 이는 경기 정보 방송 또는 주가 변동과 같은 시나리오에 종종 충분합니다.
Server-Sent Events 흐름
- 클라이언트가 일반 HTTP 를 사용하여 서버에 웹 페이지를 요청합니다.
- 클라이언트는 웹 페이지에 포함된 JavaScript 를 사용하여 서버와 연결을 설정합니다.
- 서버에 업데이트가 있으면 클라이언트에 이벤트를 보냅니다.

SSE 가 요구 사항을 충족하지 못하는 경우, 대신 WebSocket 을 사용할 수 있습니다. WebSocket 을 사용하면 브라우저와 서버 간에 전이중 (full-duplex) 통신 채널이 설정되어 TCP 소켓을 사용하는 것과 유사하게 실시간으로 양방향 메시지 교환이 가능합니다.
SSE 와 WebSocket 간의 간단한 비교
- WebSocket 은 양방향 통신을 지원하고 더 많은 고급 기능을 갖춘 전이중 통신 채널입니다. SSE 는 단방향 채널로, 서버만 브라우저로 데이터를 보낼 수 있습니다.
- WebSocket 은 새로운 프로토콜이며 서버 측 지원이 필요하지만, SSE 는 HTTP 프로토콜 위에 배포되며 기존 서버 소프트웨어에서 지원됩니다.
- SSE 는 가벼운 프로토콜이며 비교적 간단하고, WebSocket 은 더 무거운 프로토콜이며 비교적 더 복잡합니다.
실시간 웹 통신을 구현하기 위한 이러한 메커니즘에 대한 이해를 바탕으로, 이제 SSE 를 사용하여 간단한 온라인 채팅방을 구현할 것입니다.
SSE 기반 온라인 채팅방 구현
온라인 채팅방에서 메시지를 푸시하는 방법에는 여러 가지가 있으며, 이 과정에서는 Server-Sent Events (SSE) 를 사용하여 이를 구현할 것입니다. 메시지 수신을 용이하게 하기 위해 Redis 의 pub/sub (publish/subscribe, 발행/구독) 기능을 활용하여 메시지를 수신하고 보낼 것입니다. 웹 서버 측에서는 Flask 를 사용하여 구현할 것입니다.
SSE 작동 방식
이전 강의에서 SSE 가 HTTP 를 기반으로 한다는 것을 배웠습니다. 그렇다면 브라우저는 이것이 서버 전송 이벤트 스트림임을 어떻게 알 수 있을까요? 실제로 매우 간단합니다. HTTP 요청의 Content-Type 헤더를 text/event-stream으로 설정하기만 하면 됩니다. SSE 는 본질적으로 브라우저가 서버에 HTTP 요청을 보내고, 서버가 일방향 방식으로 브라우저에 정보를 지속적으로 푸시하는 것을 포함합니다. 이러한 정보의 형식 또한 매우 간단하며, "data:" 접두사 다음에 메시지 내용이 오고, "\n\n"으로 끝납니다.
Redis Pub/Sub 기능
Redis 는 캐싱, 큐잉 및 기타 서비스에 사용할 수 있는 인기 있는 인메모리 데이터베이스입니다. 이 과정에서는 Redis 의 publish/subscribe 기능을 사용할 것입니다. 간단히 말해서, 구독 기능을 사용하면 다양한 채널을 구독할 수 있으며, 이러한 채널에 새 메시지가 게시될 때마다 자동으로 수신할 수 있습니다. 서버가 POST 요청을 통해 브라우저에서 보낸 메시지를 받으면 이러한 메시지를 특정 채널에 게시합니다. 그 후, 이러한 채널을 구독한 클라이언트는 자동으로 이러한 메시지를 수신하게 되며, 이는 SSE 를 통해 클라이언트에 푸시됩니다.
기능 구현
위의 분석을 통해 채팅방의 전체 프로세스가 이미 명확해졌습니다. 이제 이 채팅방의 기능을 구현해 보겠습니다.
~/project 디렉토리에 app.py라는 파일을 생성하고 다음 소스 코드를 입력합니다.
import datetime
import flask
import redis
app = flask.Flask("labex-sse-chat")
app.secret_key = "labex"
app.config["DEBUG"] = True
r = redis.StrictRedis()
## Home route function
@app.route("/")
def home():
## If the user is not logged in, redirect to the login page
if "user" not in flask.session:
return flask.redirect("/login")
user = flask.session["user"]
return flask.render_template("index.html", user=user)
## Message generator
def event_stream():
## Create a publish-subscribe system
pubsub = r.pubsub()
## Use the subscribe method of the publish-subscribe system to subscribe to a channel
pubsub.subscribe("chat")
for message in pubsub.listen():
data = message["data"]
if type(data) == bytes:
yield "data: {}\n\n".format(data.decode())
## Login function, login is required for the first visit
@app.route("/login", methods=["GET", "POST"])
def login():
if flask.request.method == "POST":
## Store the username in the session dictionary and then redirect to the homepage
flask.session["user"] = flask.request.form["user"]
return flask.redirect("/")
return flask.render_template("login.html")
## Receive data sent by JavaScript using the POST method
@app.route("/post", methods=["POST"])
def post():
message = flask.request.form["message"]
user = flask.session.get("user", "anonymous")
now = datetime.datetime.now().replace(microsecond=0).time()
r.publish("chat", "[{}] {}: {}\n".format(now.isoformat(), user, message))
return flask.Response(status=204)
## Event stream interface
@app.route("/stream")
def stream():
## The return object of this route function must be of type text/event-stream
return flask.Response(event_stream(), mimetype="text/event-stream")
## Run the Flask application
app.run()
위의 코드에서 Flask 의 세션 기능을 사용하여 사용자 로그인 정보를 저장하고, Redis 의 publish/subscribe 기능을 사용하여 메시지를 수신하고 보내며, SSE 를 사용하여 메시지 푸시를 구현합니다.
여기서:
event_stream함수는 Redis 에서 메시지를 지속적으로 검색하여 클라이언트에 푸시하는 메시지 생성기입니다.stream함수는text/event-stream유형의 객체를 반환하는 이벤트 스트림 인터페이스로, SSE 이벤트 스트림입니다.post함수는 POST 메서드를 사용하여 JavaScript 에서 보낸 데이터를 수신하는 인터페이스입니다. 수신된 데이터를 Redis 의chat채널에 게시합니다.login함수는 첫 번째 방문에 필요한 로그인 함수입니다. 로그인에 성공하면 사용자 이름이 세션 딕셔너리에 저장된 후 홈페이지로 리디렉션됩니다.home함수는 홈페이지 라우트 함수입니다. 사용자가 로그인하지 않은 경우 로그인 페이지로 리디렉션됩니다. 사용자가 로그인한 경우index.html템플릿을 렌더링합니다.
login.html 템플릿 구현
먼저, 필요한 HTML 파일을 저장하기 위해 ~/project 아래에 templates 디렉토리를 생성합니다. templates 디렉토리에서 login.html 파일을 생성하고 다음 코드를 작성합니다.
<!doctype html>
<title>Online Chat Login</title>
<style>
body {
max-width: 500px;
margin: auto;
padding: 1em;
background: black;
color: #fff;
font:
16px/1.6 menlo,
monospace;
}
</style>
<body>
<form action="" method="post">
User Name: <input name="user" />
<input type="submit" value="login" />
</form>
</body>
login.html 파일에서 Flask 의 템플릿 기능을 활용하고 {{ user }}를 사용하여 사용자의 이름을 나타냅니다. 이 이름은 flask.session에서 가져옵니다.
index.html 템플릿 구현
다음으로, 채팅방 페이지를 구현하기 위해 templates 디렉토리에 index.html 파일을 생성해 보겠습니다. 다음 코드를 작성합니다.
<!doctype html>
<title>Online Chat</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
body {
max-width: 500px;
margin: auto;
padding: 1em;
background: black;
color: #fff;
font:
16px/1.6 menlo,
monospace;
}
</style>
<p><b>Hi, {{ user }}!</b></p>
<p>Message: <input id="in" /></p>
<pre id="out"></pre>
<script>
function sse() {
// Connect to server's event stream
var source = new EventSource("/stream");
var out = document.getElementById("out");
source.onmessage = function (e) {
out.innerHTML = e.data + "\n" + out.innerHTML;
};
}
// POST message to server
$("#in").keyup(function (e) {
if (e.keyCode == 13) {
$.post("/post", { message: $(this).val() });
$(this).val("");
}
});
sse();
</script>
index.html 파일에서 jQuery 의 $.post 메서드를 사용하여 서버로 메시지를 보내고, EventSource를 사용하여 서버 푸시 메시지를 수신합니다.
실행 및 테스트
Redis 를 사용하므로, 환경에서 Redis 서비스를 시작하고 Python 을 Redis 서버에 연결하는 데 필요한 redis 모듈을 다운로드해야 합니다.
pip install redis
sudo service redis-server start
다음으로, 채팅방을 실행할 수 있습니다.
cd ~/project
python app.py
그런 다음 브라우저에서 http://localhost:5000에 접속하여 임의의 사용자 이름을 입력하고 채팅방에 들어갈 수 있습니다.
또한 시크릿 모드 (private mode) 로 다른 브라우저 창을 열어 채팅방에 들어가 양쪽 창에서 채팅할 수 있습니다.

요약
이 프로젝트는 SSE(Server-Sent Events) 를 활용하여 실시간 웹 통신 기능을 구현하고, Flask 와 Redis 를 기반으로 온라인 채팅방을 만듭니다. 이 실험을 통해 HTTP 프로토콜 하에서의 통신 과정을 이해하는 것을 목표로 합니다.



