수학과의 좌충우돌 프로그래밍

[Django] Channels, 비동기적 채팅 구현하기 - WebSocket (1) 본문

웹프로그래밍/Django

[Django] Channels, 비동기적 채팅 구현하기 - WebSocket (1)

ssung.k 2019. 7. 10. 15:08

비동기적, 즉 실시간 채팅을 구현하기 위해서 WebSocket 을 사용하고자 하였고, django에서 이를 가능하게 해주는 Channels 라이브러리를 알아보았습니다. 전체적인 내용은 공식 문서 를 참고하였습니다.

WebSocket 이란?

WebSocket은 프로토콜로서, 실시간으로 데이터를 양방향 통신 할 수 있게 해주는 기술 입니다. Socket 이란 쉽게 생각해서 통신을 위한 통로라고 생각하면 됩니다. 이런 통로를 Web 에도 도입하게 되었는데 기존의 Web이 정보를 어떻게 주고 받는지 생각해보면 socket이 필요한 이유를 알 수 있습니다. Web 환경은 필요한 정보를 HTTP 기반으로 Request/Response로 연결하여 데이터를 주고 받아 네트워크의 연결을 유지하지 않는 특징을 가지고 있습니다. 그렇기 때문에 연결을 유지하며 실시간으로 데이터를 주고 받기에는 새로운 개념, WebSocket 이 필요하게 된 것 입니다.

 

WebSocket 이 Client 와 Server 간의 접속을 유지하는 방법은 HandShake 입니다. 이 과정을 통해 연결을 유지함으로서 방화벽과 같은 환경에서도 연결을 유지할 수 있습니다. HandShakIng 과정을 살펴보면 Client는 랜덤한 키 값을 전송하고 Server는 키 값으로 생성된 토큰을 Client에게 Response 보냅니다.

 

Channels 이란?

Channels 은 django 에서 HTTP 프로토콜이 아닌 다른 프로토콜을 사용할 수 있게 해줍니다. 위에서 알아봤듯이 WebSocket 은 프로토콜의 일종으로서 이를 사용하기 위해서 Channels 을 이용하게 됩니다. 아래 그림을 통해 일반적으로 django 가 Request/Response 을 다루는 경우와, Channels 을 사용했을 때의 차이를 알아볼 수 있습니다.

 

Channel layer 가 생겨 일반적인 HTTP message 외에도 Websocket message 를 처리할 수 있게 됩니다.

ASGI 란?

ASGI( Asynchronous Server Gateway Interface )WSGI(Web Server Gateway Interface)를 계승한 것으로 비동기 방식으로 실행되며, 웹 서버와 python 응용프로그램 간의 표준 인터페이스를 제공하기 위해 Django Channels와 배포에 사용되는 Daphne 서버에서 정의한 사양으로서 HTTP, HTTP/2 및 WebSocket와 같은 프로토콜을 지원합니다.ASGI는 비동기 요청인 웹 소켓을 처리하는 이벤트로서 connect, send, receive, disconnect가 있습니다.

 

실습

Django에 위에서 알아봤던 개념들을 적용시켜보도록 하겠습니다.

  • 준비 단계

    $ django-admin startproject mysite
    $ python manage.py startapp chat
    

    앱을 생성했으므로 앱을 추가해주겠습니다.

    # mysite/settings.py
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'chat',
    ]
    

    다음으로는 어느 채팅방으로 입장할 지 결정하는 index.html 입니다.

    input에 키워드를 입력하고 enter key 또는 button 을 누를 시, 키워드에 해당하는 채팅방으로 접속합니다.

    <!-- chat/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>
    

    index 에 해당하는 views 입니다.

    # chat/view.py
    from django.shortcuts import render
    
    def index(request):
        return render(request, 'chat/index.html', {})
    

    이에 대해서 매핑해주는 urls 입니다.

    # chat/urls.py
    from django.conf.urls import url
    from . import views
    
    urlpatterns = [
        url(r'^$', views.index, name='index'),
    ]
    
    # mysite/urls.py
    from django.conf.urls import url, include
    from django.contrib import admin
    
    urlpatterns = [
        url(r'^chat/', include('chat.urls')),
        url(r'^admin/', admin.site.urls),
    ]
    
  • Channels 라이브러리

    우선 channels 라이브러리를 설치하도록 하겠습니다.

    $ pip install -U channels
    

    다음으로는 라우팅 config 를 작성하겠습니다. 새로운 파일을 만들어주고 아래 내용을 추가해줍니다.

    # mysite/routing.py
    from channels.routing import ProtocolTypeRouter
    
    application = ProtocolTypeRouter({
        # (http->django views is added by default)
    })
    
    

    이제 settings 에 channels 앱을 등록하고, ASGI_APPLICATION 을 추가해줍니다. 여기서 주의할 점은 channels 개발 서버가 다른 앱과 충돌할 수 있으므로, 가장 위에 추가해주도록 합시다.

    INSTALLED_APPS = [
        'channels',
        'chat',
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]
    
    # Channels
    ASGI_APPLICATION = 'mysite.routing.application'
    

    이제 channels 은 runserver 명령을 제어하고 django 개발 서버를 Channel 개발 서버로 대처하게 됩니다. 서버를 돌리게 되면 기존과 다른 방식으로 서버가 돌아가는 걸 확인할 수 있습니다.

    Django version 2.2.3, using settings 'mysite.settings'
    Starting ASGI/Channels version 2.2.0 development server at http://127.0.0.1:8000/
    
  • 채팅해보기

    현재는 채팅방에 접속을 해도 채팅방이 존재하지 않으므로 에러가 발생합니다. 채팅방에 해당하는 room.html을 만들어봅시다.

    <!-- chat/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"/>
    </body>
    
    <script>
        var roomName = {{ room_name_json }};
    
        var chatSocket = new WebSocket(
            'ws://' + window.location.host +
            '/ws/chat/' + roomName + '/');
    
        chatSocket.onmessage = function(e) {
            var data = JSON.parse(e.data);
            var message = data['message'];
            document.querySelector('#chat-log').value += (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) {
            var messageInputDom = document.querySelector('#chat-message-input');
            var message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
    
            messageInputDom.value = '';
        };
    </script>
    
    </html>
    

    room 에 해당하는 view 입니다.

    # chat/views.py
    
    from django.shortcuts import render
    from django.utils.safestring import mark_safe
    import json
    
    def index(request):
        return render(request, 'chat/index.html', {})
    
    def room(request, room_name):
        return render(request, 'chat/room.html', {
            'room_name_json': mark_safe(json.dumps(room_name))
        })
    

    이를 매핑해주는 urls도 추가해줍니다.

    # chat/urls.py
    
    from django.conf.urls import url
    from . import views
    
    urlpatterns = [
        url(r'^$', views.index, name='index'),
        url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),
    ]
    

     

    현재까지 상황을 정리해보면, 원하는 채팅방으로 접속이 가능하고

     

채팅방에서 메세지를 작성함으로 대화를 할 수 있습니다. 

하지만 글을 입력하고 전송을 하면, 에러가 발생합니다. 아직 웹소캣 소비자가 만들어지지 않았기 때문에 에러가 발생하는 것이 정상입니다. 

이에 대해서는 다음 포스팅에서 이어서 알아보도록 하겠습니다.

Comments