본문 바로가기
Python/Django

[Django]WebSocket을 활용한 실시간 랭킹 구현

by Byeong 2024. 12. 7.

 목표

실시간 접속자 수를 표시하고 이를 바탕으로 채팅방 랭킹을 제공하고 싶었다.

 

구현 목표

1. 실시간 접소자 표시 : 홈 화면의 각 채팅방마다 접속자 수를 표시

2. 접속자 수에 따른 랭킹 제공 : 접속자가 많은 순서대로 랭킹을 실시간으로 표시

 

홈 화면에서 채팅방 리스트에 실시간으로 채팅방의 접속자 수를 표시하고 또 접속자 수에 따라 랭킹이 기능을 구현하고 싶었다. 

하지만 채팅방 접속/퇴장 시마다 서버에 요청을 보내야 했는데, 이는 서버 부화를 유발할 가능성이 있다고 생각했다. 

 

이를 해결하기 위해 WebSocket을 활용해 API 요청을 줄이고 실시간 데이터를 주고받는 방식으로 구현하기로 했다.

 


구현 방법 

 

1. WebSocket Consumers 구조

-myProj
 -app
  -consumers
   -chat_consumers.py
   -home_consumers.py
  -routing.py

Cunsumers 디렉터리 아래에  chat_consumers.py와 home_consumers.py를 생성.

 

2. routing.py

#routing.py
...
websocket_urlpatterns = [
    re_path(r"ws/chat/(?P<room_name>\w+)/$", ChatConsumer.as_asgi()),
    re_path(r"ws/chat/", HomeConsumer.as_asgi()),
]

routing.py를 통해 WebSocket URL과 Consumers를 연결.

 

   routing.py는 Django의 urls.py와 비슷한 역할을 한다.
   consumers.py는  Django의 views.py와 비슷한 역할을 한다.

 

3. Home_Consumers.py

#home_consumers.py
...
class HomeConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.home = "home"
        await self.channel_layer.group_add(self.home, self.channel_name)
        await self.accept()

        #홈화면 갱신
        send_room_list_celery.delay()


    #현제 채널 그룹에서 제거
    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.home, self.channel_name)

홈 화면에 접속한 사용자들에게 채팅방 리스트와 랭킹 정보를 전송

 


Celery를 활용한 문제 해결

문제

 웹소켓 연결 상태에서는 빠른 데이터 전달이 가능했지만, 채팅방을 이동하여 새로운 웹소켓에 접속할 때 약간의 지연 시간이 발생했다. 이로 인해 홈 화면에서 데이터 갱신 시점과 채팅방 접속 시 데이터 추가 시점 간에 시간 차이가 생겨 접속자 수 데이터가 불일치하는 문제가 발생했다. 이를 해결하기 위해 채팅방 접속 후 데이터를 갱신하는 방식을 선택했다.

 

 

해결 방법 

채팅방 접속 시 Celery를 활용해 랭킹과 리스트를 비동기적으로 갱신 

 

1. tasks.py

#tasks.py

...
#그룹에 보내는 메시지 비동기 처리
@app.task()
def send_room_list_celery():
    channel_layer = get_channel_layer()

    #모든 채틷방 가져오기
    room_list_db = ChatRoom.objects.all()

    #Redis에서 모든 접속자 가져오기
    room_list_redis = redis_client.zrevrange('chatroom_ranking',0,-1, withscores=True)

    #{chat_room_id : 접속자 수} 형식으로 변경 
    list_dict = { 
            name.decode('utf-8').split('.')[1]:length
            for name, length in room_list_redis
            }
    
    #Redis에 있는 사용자들 가져와서 입력
    list_rooms=[]
    for room in room_list_db:
                list_rooms.append({
                    "name": room.name,
                    "id":room.id,
                    "users": list_dict.get(str(room.id), 0),
                })
                

    #{chat_room_id : 접속자 수} 형식으로 변경 + 1~10위 가져오기
    rank_dict ={name.decode('utf-8').split('.')[1]:length
                        for name, length in room_list_redis[:10]}
    
    #딕셔너리 순서에 맞게 가져오기 (ZSET이라 랭킹 순서)
    rank_rooms=[]
    for k in rank_dict:
            for room in room_list_db:
                if room.id == int(k):
                    rank_rooms.append({
                        "name": room.name,
                        "id":room.id,
                        "users": rank_dict.get(str(room.id))
                    })

    #홈 랭킹, 리스트 갱신
    async_to_sync(channel_layer.group_send)(
        "home",
        {
            "type": "send_chatroom_list",
            "room_list": list_rooms,
            "room_rank": rank_rooms,
        }
    )

 

채팅방 리스트와 랭킹 정보를 Redis에서 가져와 WebSocket 그룹에 전송.

Redis ZSET 활용: 접속자 수를 기준으로 채팅방 데이터를 관리하며, 랭킹 순서대로 가져온다. - ZSET 추가 설명

 

 

 

2. chat_consumers.py

#chat_consumers.py
...
async def connect(self):
    ...      
    #홈화면 갱신
    send_room_list_celery.delay()

 

채팅방 접속시 홈 화면의 리스트와 랭킹을 갱신을 통해  데이터 조회에서 발생하는 불일치를 차단

 


결과

Websocket을 통해 API 요청을 줄이고 실시간 데이터를 반영했고 또 Celery를 통해 데이터 갱신 지연 문제를 해결했다.


느낀 점 

 Websocket은 채팅 기능 이외에도 다양한 환경에서도 사용이 가능하다고 느꼈고 특히 양방향 통신에서는 유용하다고 생각했다.

다만 연결을 지속적으로 유지를 통해 발생하는 문제들을 고려해야 한다고 생각했다.

 

추후 구현 과정에서 접속 시간이 다소 길어지는 문제가 발생했는데, 이 부분을 개선하는 작업이 필요하다고 느꼈다.