Flask 와 Redis 를 이용한 실시간 채팅방

PythonBeginner
지금 연습하기

소개

이 프로젝트는 SSE (Server-Sent Events) 와 Redis 를 활용하여 온라인 채팅방을 구현합니다. Python 및 JavaScript 구문에 대한 사전 지식과 Flask 및 Redis 사용에 대한 기본적인 이해가 필요합니다.

이 실험 섹션에서는 다음 개념들을 배우고 실습할 것입니다:

  • 웹 실시간 통신
  • SSE 의 작동 방식
  • Redis 활용

👀 미리보기

온라인 채팅방 인터페이스 미리보기

🎯 과제

이 프로젝트에서 다음을 배우게 됩니다:

  • Flask 와 SSE 를 사용하여 간단한 온라인 채팅방을 만드는 방법
  • 사용자 로그인 기능을 구현하는 방법
  • 메시지 저장 및 검색을 위해 Redis 를 사용하는 방법

🏆 성과

이 프로젝트를 완료하면 다음을 수행할 수 있습니다:

  • 웹 애플리케이션에서 실시간 통신을 위해 SSE 를 설정
  • 채팅방 애플리케이션에서 메시지를 저장하고 검색하기 위해 Redis 를 사용
  • Flask 에서 사용자 로그인 기능 구현

웹 실시간 통신

웹 실시간 통신 (Web Real-Time Communication, WebRTC) 은 사용자가 페이지를 새로 고칠 필요 없이 웹 페이지에서 이벤트에 대해 즉시 알릴 수 있는 메커니즘을 의미합니다. WebRTC 는 실시간 채팅 및 인스턴트 메시징과 같은 다양한 용도로 사용됩니다.

웹 클라이언트와 서버 간의 통신에는 여러 가지 방법이 있습니다.

일반 HTTP 흐름

  1. 클라이언트가 서버에 웹 페이지를 요청합니다.
  2. 서버가 이에 따라 응답합니다.
  3. 서버가 응답을 클라이언트에 다시 보냅니다.

HTTP 요청 응답 흐름

HTTP 요청은 상태 비저장 (stateless) 이므로, 각 요청 후에 HTTP 연결이 종료됩니다. 따라서 서버와 브라우저는 관련 정보를 업데이트하기 위해 다음 요청이 이루어질 때까지 서로를 완전히 인식하지 못합니다. 이 시점에서 브라우저가 주기적으로 요청을 보내 실시간 효과를 시뮬레이션할 수 있는 간단한 솔루션을 생각하는 것은 어렵지 않습니다. 이를 폴링 (polling) 이라고 합니다.

폴링 흐름

  1. 클라이언트가 일반 HTTP 를 사용하여 서버에 연결 요청을 보냅니다.
  2. 클라이언트는 웹 페이지에 포함된 JavaScript 폴링 스크립트를 실행하여 정보를 검색하기 위해 서버에 주기적으로 요청을 보냅니다 (예: 5 초마다).
  3. 서버는 각 요청에 응답하고 일반 HTTP 요청과 마찬가지로 해당 정보를 다시 보냅니다.

폴링 통신 다이어그램

폴링을 사용하면 거의 실시간으로 정보를 얻을 수 있습니다. 그러나 폴링으로 인해 브라우저에서 서버로의 빈번한 요청은 성능 비효율성을 초래할 수 있습니다. 이러한 문제를 완화하기 위해 대안적인 방법이 제안되었습니다. 클라이언트 요청에 즉시 응답하는 대신, 서버는 데이터 변경 (또는 시간 초과) 이 발생할 때까지 응답을 반환하지 않습니다. 이 접근 방식은 폴링에서 요청 수를 줄이기 위해 연결 유효성을 최대화합니다. 이 방법을 롱 폴링 (long polling) 이라고 합니다.

롱 폴링 흐름

  1. 클라이언트가 일반 HTTP 를 사용하여 서버에 웹 페이지를 요청합니다.
  2. 클라이언트는 웹 페이지에 포함된 JavaScript 스크립트를 실행하여 데이터를 보내고 서버에 정보를 요청합니다.
  3. 서버는 클라이언트의 요청에 즉시 응답하는 대신 유효한 업데이트를 기다립니다.
  4. 정보가 업데이트되고 유효하면 서버는 데이터를 클라이언트에 푸시합니다.
  5. 서버의 알림을 받으면 클라이언트는 즉시 새 요청을 보내 다음 폴링 라운드를 시작합니다.

롱 폴링 흐름 다이어그램

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

Server-Sent Events 흐름

  1. 클라이언트가 일반 HTTP 를 사용하여 서버에 웹 페이지를 요청합니다.
  2. 클라이언트는 웹 페이지에 포함된 JavaScript 를 사용하여 서버와 연결을 설정합니다.
  3. 서버에 업데이트가 있으면 클라이언트에 이벤트를 보냅니다.

SSE 통신 흐름 다이어그램

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) 로 다른 브라우저 창을 열어 채팅방에 들어가 양쪽 창에서 채팅할 수 있습니다.

Chatroom interface screenshot

요약

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

✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습