장고 실시간 채팅(Channels와 redis server 이용)

Pywiki
Sam (토론 | 기여)님의 2022년 10월 17일 (월) 14:45 판 (→‎채팅어플 만들기)
둘러보기로 가기 검색하러 가기

1 개요

장고 채널 기능의 활용을 위해 간단한 채팅 앱 만들어보기.

1.1 사전 준비

과정 설명 방법
장고 장고가 설치되어 있다고 가정한다. 1. 장고 개요
채널 설치 채널이 설치되어 있다고 가정한다. 장고 channels

2 기초준비

2.1 채팅어플 만들기

패키지설치부터 진행하기엔 중간점검이 어려워 틀 만들기를 먼저 수행한다.

과정 설명 방법
어플리케이션생성 채팅을 위한 앱을 생성한다. django-admin startapp chat
앱 등록 settings.py에 추가.
INSTALLED_APPS = [
    ...
    'chat',
폴더 정리 - 앱을 생성하고 생성된 __init__.py와 views.py를 제외한 모든 것들을 지운다.

(채팅기능만을 위해선 나머지는 필요 없어, 지워도 된다. 근데 그냥 두자.)

- 탬플릿을 담기 위해 templates/chat 디렉토리를 생성해준다.

urls.py 생성 및 매핑 앱 내에서 사용되는 url을 다루기 위해 urls.py를 생성한다. chat/urls.py의 내용.
from django.urls import path
from . import views
app_name = 'chat'  # 보통 앱이름을 써서 url을 구분하지만, 

urlpatterns = [
    path('', views.index, name='index'),
]
기초 urls.py 수정
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
]

여기까지 하고 runserver 후 제대로 작동하는지 확인하자.

2.2 채팅룸 구현

2.2.1 채팅 인덱스 구현

들어갈 채팅룸을 입력하는 공간.

과정 설명 방법
채팅 인덱스 탬플릿 작성 채팅룸을 입력하기 위한 인덱스 탬플릿.

templates/chat/index.html로 작성하자.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    What chat room would you like to enter?<br>
    <input id="room-name-input" type="text" size="100"><br>
    <input id="room-name-submit" type="button" value="Enter">

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#room-name-submit').click();
            }
        };

        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>
뷰 작성
from django.shortcuts import render

def index(request):
    return render(request, 'chat/index.html')

2.2.2 채팅룸 구현

과정 설명 방법
탬플릿 작성 templates/chat/room.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br>
    <input id="chat-message-input" type="text" size="100"><br>
    <input id="chat-message-submit" type="button" value="Send">
    {{ room_name|json_script:"room-name" }}
    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);

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

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        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();
            }
        };

        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
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>
뷰 작성 뷰에 다음 함수를 추가한다.
def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name
    })
URL 다음의 내용을 urls.py에 추가한다.
path('<str:room_name>/', views.room, name='room'),

2.2.3 참가자 구현

과정 설명 방법
참가자 파일 작성 /chat/consumers.py 작성.

모든 요청을 받아들이는 비동기적인 웹소켓.

클라이언트로부터 메시지를 받아서 그대로 전달.

import json
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))
라우팅 작성 /chat/routing.py 작성
from django.conf.urls import url
from . import consumers

websocket_urlpatterns = [
    url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
]
라우팅 등록 /routing.py 수정

chat 안의 라우팅을 등록해준다.

그리고 임포트하는 모듈을 수정해준다.

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

여기까지 하고 채팅을 쳐 보면... 채팅이 나와야 정상.


2.2.4 체널레이어 구현

과정 설명 방법
패키지 설치 Channels가 Redis인터페이스를 인식하도록. pip install channels_redis
settings.py 설정 ASGI 아래 추가하자.
ASGI_APPLICATION = 'routing.application'  # routing.py 파일의 application을 불러온다.
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}


2.3 개요

레디스 서버를 이용한 실시간 채팅 구현

2.3.1 사전설정

과정 설명 방법
서버 설치 apt-get install redis-server
서버 테스트 서버가 잘 설치되었는지 테스트.

서버 구동에 대한 메시지가 뜬다.

redis-server
채널 라이브러리 설치 장고에서 사용할 채널 라이브러리를 설치한다. pip install -U channels
연동 라이브러리 설치 서버와 채널을 연동할 라이브러리를 설치한다. pip install channels_redis
앱을 만든다. django-admin startapp chat
앱 등록 settings.py에 추가. INSTALLED_APPS 가장 위에 'channels', 'chat'을 맨 위에 추가해준다.

다른 서드파티 앱과 충돌할 수 있어 가장 처음에 둔다.

세팅 settings.py 안에 추가.
ASGI_APPLICATION = 'config.routhing.application'

CHANNEL_LAYERS = {
    'default':{
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        }
    }
}

os.environ["DJANGO_ALLOW_ASYNC_UNDAFE"] = "true"
을 추가해준다.(대강 스테틱 설정 아래면 적당)

2.4 모델작성

chat 앱의 models.py 작성

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

class Room(models.Model):
    room_name = models.CharField(max_length=100, blank=True)
    users = models.ManyToManyfield(
        settings.AUTH_USER_MODEL,  # 유저모델과 연결한다.
        blank=True,
        related_name = 'rooms')  # 룸이라는 인덱스 지정.
    
    def __str__(self):
        return self.room_name
        
class Message(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=1)
    room = models.ForeignKey(Room, related_name='messages', default=1, on_delete=models.CASCADE)
    content = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.content

chat앱의 admin.py 작성

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

@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
    list_display = ['id', 'room_name']
    list_display_links = ['room_name']
    
@admin.register(Room)
class MessageAdmin(admin.ModelAdmin):
    list_display = ['user', 'room', 'content', 'created_at']
    list_display_links = ['user', 'room', 'content', 'created_at']

대화모델 작성(보통 친구모델 등에 작성된다.)(근데 그냥 chat 앱 안에 작성하면 안되나...?)

...
from .chat.models import Room, Message

class chat_connection(models.Model):
    target = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE)  # 상대방
    room = models.ForeignKey(Room, blank=True, on_delete=models.SET_NULL, null=True)  # null옵션이 있어야 하나..? 없어도 될듯, 강의에선 친구모델 위에 더하느라 붙인듯.
    user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE)  # 나
    created_at = models.DateField(auto_now_add=True)
    
    def __str__(self):
        return self.user.username + "채팅"  # 사용자명을 반환한다.

2.5 뷰 작성

from django.shortcuts import render, get_object_or_404
forom django.contrib.auth import get_user_model
from chat.models import *
import logging

def start_chat(request):
    from_user =   # 본인의 유저모델 가져오기
    to_user = 유저모델.objects.get(pk=target_request_id)  # 상대의 유저모델 가져오기(이름이나 pk로 불러오게 하면 될듯)
    
    room_name = "{},{}".format(from_user.username, to_user.username)
    room = Room.objects.create(room_name=room_name)  # 룸을 만든다.
    
def chat_list(request):
    user = request.user
    user_profile = user.user_profile
    friends = user.friends.all()  # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
    
    context = {'user_profile': user_profile,
                'friends':friends,
    }
    
    return render(request, 'chat/chat_list.html', context)
    
def room(request, room_id):
    user = request.user
    user_profile = user.profile
    friends = user.friends.all()  # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
    
    room = Room.objects.get(pk=room_id)  # 룸 모델에서 해당 pk에 맞는 룸 불러오기
    friends_uer = room.users.all().exclude(pk=user.id).first()
    
    context = {'current_user': user,
        'user_profile': user_profile,
        'friends':friends,
        'room':room,
        'friends_user': friends_user,
    }
    
    return render(request, 'chat/room.html', context)

2.6 url 작성

config의 url을 넘김 처리하고..

chat앱 안의 urls.py를 작성한다.

from .views import *

app_name= 'chat'

urlpatterns = [
    path('', chat_list, name='chat_list'),
    paht('<str:room_id', room, name='room'),
]

2.7 탬플릿 작성

chat 앱 안의 templates 폴더를 만든 다음 넣는다.

<!--확장코드-->

<!--들어가야 할 것들-->
<script src="{% static 'js/messenger.js' %}"></script>

<div id="user_list">
    {% for friend in friends %}
        {% if friend.room %}
         <li name="{{ friend.room.id }}"><a href="/chat/{{ friend.room.id }}/"
         <div>{{ friend.user }}</div>
</div>

<div id="time_line">
    <div id="main_section">
        <div id="feed">
            <div id="text_field">
                <input type="text" id="txt">
                <button type="submit" id="btn">전송</button>
            </div>
        </div>
    </div>
</div>