Django/Django study

[Django]웹소켓을 이용한 채팅기능

카늬 2022. 12. 23. 19:50

사용자간의 채팅기능을 구현하고싶어 알아본 결과 크게 2가지를 이용하여 채팅을 구현하는데 첫째로 AJAX를 이용한 것이였고 두번째로는 WebSocket(웹소켓)을 이용한 구현이 많았는데 필자는 WebSocket을 이용하여 채팅을 구현해보려 했다 하지만 참고한 블로그가 내가 처음기획한 채팅기능과 매우 흡사하여 사실상 다른 블로그에서 가져와 내가 필요한 부분만을 바꾼것 뿐이므로 제대로된 동작에 대해 보고 싶다면 해당 링크로 이동하여 보는 것을 추천한다.

Django channels 실시간 채팅 기능 (websocket) (tistory.com)

 

Django channels 실시간 채팅 기능 (websocket)

공식문서 + 구글링 + 유튜브를 통해 실시간 채팅 기능 구현(서버 api 동기식) 스파르타 내배캠 4번째 팀 프로젝트 실시간 채팅 기능 담당. 간단하게 프로젝트 소개를 하고 Django channels를 이용한 실

a-littlecoding.tistory.com

 

먼저 웹소켓은 양방향 통신 또는 데이터 전송을 가능하게 하는 기술로 보통 HTTP는 한방향 통신인 request/response기반으로 동작하는데 다른쪽에서 업데이트시 이미 띄워진 화면에서는 아무런 변화가 없어 상황을 바로바로 알 수 없지만  WebSocket을 이용하면 이러한 문제를 해결할 수 있다. Ajax를 사용해도 어느정도 해결이 가능하다고 하는데 빠른 업데이트를 위해서는 WebSocket이 좋다고 한다.

 

 

이 글은 Django Channels tutorial을 만든 다음에 작업이므로 아래 사이트에 접근하여 직접 만들어 본 다음 보면 좋을것 같다.

Tutorial Part 1: Basic Setup — Channels 4.0.0 documentation

 

Tutorial Part 1: Basic Setup — Channels 4.0.0 documentation

So far we’ve just created a regular Django app; we haven’t used the Channels library at all. Now it’s time to integrate Channels. Let’s start by creating a routing configuration for Channels. A Channels routing configuration is an ASGI application

channels.readthedocs.io

 

models.py

from django.db import models
from django.conf import settings
from common.models import User



class Room(models.Model):
    name = models.CharField(max_length=10,null=True)
    user= models.ManyToManyField(User,related_name='user_id')

    class Meta:
        db_table = "room"
        
    def __str__(self):
        return self.name

class Message(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE,related_name='author_messages')
    content = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)
    room_id = models.ForeignKey(Room, on_delete=models.CASCADE, db_column="room_id",null=True)

    def __str__(self):
        return self.content
    
    def last_10_message(self,room_id):
        
        message_range = 10
        
        Message_list = Message.objects.filter(room_id=room_id).order_by('timestamp')
        Message_list=Message_list[len(Message_list)-message_range:]
        return Message_list

모델은 채팅방과 채팅방에서 사용할 메시지이며 def last_10_message() 함수는 채팅방에 접근하였을때 기존에 있던 가장 최근 채팅 10개를 가져오는 함수다.

 

admin.py

# admin.py
from django.contrib import admin
from .models import Message,Room

admin.site.register(Message)
admin.site.register(Room)

 

view.py

from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from django.http import HttpRequest, HttpResponse
from collections import Counter



from .models import Room,Message
from common.models import User,Profile

def mainpage(request):
    return render(request, "chat/html/index.html")
# Create your views here.

#채팅방보기
@login_required
def view_room(request: HttpRequest, room_name:str) -> HttpResponse:
    room_id = int(room_name)
    try:
        user_prifile = Profile.objects.get(user=request.user)
        return render(request,'chat/html/room.html',{
            "room_name": room_id,
            "user_prifile":user_prifile})
    except:
        return render(request,'chat/html/room.html')
    
    
   

#채팅방 생성
@login_required
def create_room(request: HttpRequest, user_id:int) -> HttpResponse:
    user1 = User.objects.get(id = request.user.id)
    user2 = User.objects.get(id = user_id)
    
    #1번,2번 유저가 참여한 모든 방을 가져옴
    find_room_qs = Room.objects.filter(user__in=[user1.id,user2.id])
    
    find_room_list=[]
    for find_room in find_room_qs:
        find_room_list.append(find_room.id)
    
    result = Counter(find_room_list)
    for key,value in result.items():
        if value >= 2:
            #기존에 채팅방이 존재할 경우 이동
            return redirect("ChatApp:view_room",room_name=str(key))
        
        
    roomname = 'test'
    room = Room.objects.create(name = roomname)
    room.user.add(user1,user2)
    room_id = room.id
    #기존에 채팅방이 존재하지 않을경우 이동
    return redirect("ChatApp:view_room",room_name=str(room_id))

친구사이에 채팅을 시도 할 경우 우선 현재 로그인하고 있는 유저와 대화를 원하는 유저의 정보를 이용해 채팅방을 생성하거나, 기존 채팅방이 존재할경우 접근을 시도한다. 그 후 채팅방 id를 이용해 기존 채팅이 존재할경우 기존채팅을 반환하며 존재하지 않을경우는 아무런 정보없이 채팅창으로 가게된다.

 

urls.py

from django.urls import path

from . import views

app_name = "ChatApp"

urlpatterns = [
    path("api/<int:user_id>/",views.create_room ,name="create_room"),
    path("<str:room_name>/",views.view_room,name="view_room"),
]

늘 하던 그 맛이다.

 

consumers.py

import json

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from .models import Message,Room
from common.models import User,Profile


class ChatConsumer(WebsocketConsumer):
    
    def fetch_messages(self,data):#채팅기록 가져오기
        room_id = int(self.room_name)
        messages = Message.last_10_message(self,room_id=room_id)
        content = {
            'command':'messages',
            'messages':self.messages_to_json(messages)
        }
        self.send_message(content)
        
    
    def new_messages(self,data):#채팅 보내기
        user_id = data['user_id']
        room_id = int(self.room_name)
        user_contact = User.objects.filter(id = user_id)[0]
        room_contact = Room.objects.filter(id = room_id)[0]
        message_create = Message.objects.create(
            author = user_contact,
            room_id = room_contact,
            content = data['message']
            )
        content = {
            'command':'new_messages',
            'message': self.message_to_json(message_create)
        }
        return self.send_chat_message(content)
    
    def messages_to_json(self,messages):
        result = []
        for message in messages:
            result.append(self.message_to_json(message))
        return result
    
    def message_to_json(self,message):
        return {
            'author':message.author.username,
            'content':message.content,
            'timestamp':str(message.timestamp)
        }
    commands = {
        "fetch_messages" : fetch_messages,
         "new_messages" : new_messages
    }
    
    # websocket 연결
    def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        self.room_group_name = "chat_%s" % self.room_name

        # Join room group / 그룹에 참여
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
            )
        # websocket 연결을 수락 / connect() 메서드 내에서 accept()를 호출하지 않으면 연결이 거부되고 닫힌다.
        self.accept()
        
    # websocket 연결 해제
    def disconnect(self, close_code):
        # Leave room group/ 그룹에서 탈퇴
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name, 
            self.channel_name
            )

    # Receive message from WebSocket
    def receive(self, text_data):
        data = json.loads(text_data)
        self.commands[data['command']](self,data)
        
        
    def send_chat_message(self,message):
        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name, 
            {
                "type": "chat_message",
                "message": message
            }
        )


    def send_message(self,message):
        self.send(text_data=json.dumps(message))
        
    # Receive message from room group
    def chat_message(self, event):
        message = event["message"]

        # Send message to WebSocket
        self.send(text_data=json.dumps(message))

consumers.py는 웹소켓과 소통하는 역할을 맡고있다.

connect() : 웹소켓과 연결되었을 경우 실행

connect() : 웹소켓과 연결이 해제되었을 경우 실행

receive() : 클라이언트에게서 입력이 들어왔을경우 실행

 

room.html

{% extends 'base/base.html' %}
{% block content %}
    <div class="container ">
    <textarea class="form-control" id="chat-log" disabled cols="100" rows="20" style="resize: none;"></textarea><br>
    <input class="form-control" id="chat-message-input" type="text" size="100"><br>
    <input class="btn btn-outline-secondary" id="chat-message-submit" type="button" value="전송">
    </div>




    {{ room_name|json_script:"room-name" }}
    {{ user.id|json_script:"user_id"}}
    {{ user.profile.nickname|json_script:"nickname"}}

    
    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);
        const user_id = JSON.parse(document.getElementById('user_id').textContent);
        const nickname = JSON.parse(document.getElementById('nickname').textContent);

        const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/chat/'
            + roomName
            + '/'
        );

        // chatSocket에 onopen 메소드 지정
        chatSocket.onopen = function (e) {
            fetchMessages();
        }
        // chat-log id를 통해서 기존 message 에 추가해서 message 를 onmessage 해줌
        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            
            if(data['command']==='messages'){
                for (let i = 0; i < data['messages'].length;i++){
                    createMessage(data['messages'][i]);
                }
            }
            else if(data['command']==='new_messages'){
                createMessage(data['message']);
            }
            
        };

        // 에러났을 때는 onclose
        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        // 엔터를 눌러도 click 이벤트가 발생하게 처리
        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };

        // onclick 이벤트가 발생하면 input value를 message에 저장해서 json형태로 chatSocket으로 전송
        // chatSocket 전송이 완료되면 input box value 를 공백으로 초기화
        document.querySelector('#chat-message-submit').onclick = function(e) {
            const messageInputDom = document.querySelector('#chat-message-input');
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message,
                'user':nickname,
                'user_id':user_id,
                'command': "new_messages"

            }));
            messageInputDom.value = '';
        };

        function fetchMessages(){
            chatSocket.send(JSON.stringify({'command':'fetch_messages'}))
        }

        function createMessage(data){
            const author = data["author"];
            const message = data["content"];
            const time = data["timestamp"];
            document.querySelector('#chat-log').value += ("[" + time.substr(0,19) + "] " + author + " : " + message + '\n');
        }
    </script>

    {% endblock %}

 

실행화면

채팅로그에 시간이 있는데 이 부분은 sqllite에 설정되어있는 나라에서 우리나라가 9시간이 차이가 나는 문제가 있다고 한다.

 

이번 채팅을 구현하는데 2주정도의 시간이 들었다. 웹소켓에대한 개념도 없었으며 참고할만한 자료는 한국어로 되어있는 글을 위에 올린 저 글 하나밖에 못찼았고 한국어로 들어도 이해하기 힘든 내용을 유튜브 자동자막으로 흘려보내듯이 계속 공부를 하였다. 그래도 계속 하다보니 처음에는 무슨소리인지 1도 모르겠는 내용도 점차 이해가 되어 어찌어찌 완성을 하였다. 개발을 하면서 처음에는 봐도 모르니 당장이라도 때려치고 싶은 기분이 들지만 이렇게 이해가 되는 과정은 참 좋은것 같다는 생각이다.

 

 

'Django > Django study' 카테고리의 다른 글

[Django] 팔로우 언팔로우  (1) 2022.12.10
[Django] 친구추가, 팔로우 목록조회  (0) 2022.12.07
[Django]회원 프로필 수정  (0) 2022.11.27
[Django] 회원 프로필 만들기  (0) 2022.11.26
[Django] User Custom  (0) 2022.11.24