새소식

반응형
Toy Project/Django Web Chatting

[Django] 장고 웹 채팅 서비스 - Channels 주요 구성 요소

  • -
반응형

장고 로고 이미지입니다.
Django

 

장고 웹 채팅 서비스 - Channels 주요 구성 요소


Channels를 이용하여 장고 웹 채팅 서비스를 만들기에 앞서 Channels를 구성하는 주요 패키지에 대해 먼저 알아보는 시간을 가져보자. 

 

패키지 목적
channels(필수) 장고의 통합 레이어
daphne(필수) ASGI 서버로써 채널스 4.0부터 장고/채널스 개발서버로서 사용된다. 또한 실서비스에서는 daphne 명령이나 gunicorn/uvicorn 명령을 사용하여, 장고 서버를 구동한다.
channels_redis(옵션) Channels 구동에 필수는 아니지만, 채팅 서비스에서는 프로세스간 통신이 필요하기에 필수이다.

 

위의 표에서 확인할 수 있듯이, channels와 daphne는 필수 라이브러리이다. channels 버전 4.0부터 runserver 명령은 channels가 아니라 daphne를 통해 수행된다. 그리고 채널스에서 레디스 활용을 위해 channels_redis 라이브러리가 필요하다. 해당 라이브러리를 통해 프로세스간 통신이 가능해진다. 

 

scope


 

현재 요청의 세부내역이 담긴 사전(dict)

 

장고의 처리 주체는 뷰 함수/클래스


# views.py
# 장고 함수 기반 뷰
def chatroom_list(request):
    # ...
    
# 장고 클래스 기반 뷰에도 HttpRequest가 있다.
from django.views.generic import ListView

class ChatRoomListView(ListView):
    # ...
    

# urls.py
urlpatterns = [
    path("chat1/", chatroom_list),
    path("chat2/", ChatRoomListView.as_view()),
]

 

장고 기본에서는 HTTP 요청을 처리하는 주체는 View이다. 위의 코드에서 볼 수 있듯이 함수와 클래스 형태로 구현하였다. 하지만 채널스에서는 HTTP와 웹소켓 요청을 처리하는 주체가 아래의 코드에서 볼 수 있듯이 Consumer 클래스가 된다. 이때, 함수 구현은 지원되지 않으며, 클래스로만 구현할 수 있다. 

from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        ...
        
    def receive(self, text_data = None, bytes_data = None):
        self.send(text_data, bytes_data)
        
    def disconnect(self, code):
        ...
        
websocket_urlpatterns = [
    path("ws/chat/<str:room_name>/", consumers.ChatConsumer.as_asgi()),
]

 

 

scope과 장고 뷰의 HttpRequest는 유사한 역할


# views.py

# 장고 함수 기반 뷰
def chatroom_list(request):
    request.user    # 현재 요청의 User 인스턴스
    request.session # 세션 객체
    request.COOKIES # 쿠키
    request.headers # 헤더
    request.GET     # URL Captured value
    request.POST    
    request.FILES
    
    # ...
    
# 장고 클래스 기반 뷰에도 HttpRequest는 있다.
from django.views.generic import ListView

class ChatRoomListView(ListView):
    def get(self, **kwargs):
        self.request.user
        self.request.session
        self.request.COOKIES
        self.request.headers
        self.request.GET
        
        # ...

 

View에서는 HttpRequest 객체를 통해서 유저/세션/쿠키/헤더 등의 현재 요청의 모든 내역을 조회할 수 있다. 하지만 Consumer Instance에서는 아래의 예제 코드와 같이 self.scope 사전을 통해 현재의 요청의 모든 내역을 조회할 수 있다. 

 

from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.user = self.scope["user"]    # 현재 요청의 user 인스턴스
        self.scope["session"]             # 세션 객체
        self.scope["cookies"]             # 쿠키(dict)
        self.scope["headers"]             # 헤더(list)
        self.scope["url_route"]           # URL Captured value(dict)
        
        if self.user.is_authenticated:
            # ...
            self.user.username
            self.user.email
            
        room_name = self.scope["url_route"]["kwargs"]["name"]
        # ...

 

 

Channels 주요 구성요소


이제 본격적으로 channels의 주요 구성요소에 대해 한 번 살펴보자. 

 

Consumers


Consumer 클래스는 채널스에서 요청을 처리하는 주체로서 일관된 처리방법을 제시한다. 웹소켓/HTTP 프로토콜을 처리하는 기능과 채널레이어를 통해 메시지를 보내고 받는 부분까지 모두 지원해 주기에 반복을 줄이고, 최소한의 코드로 비즈니스 로직에 더욱 집중할 수 있도록 도와준다. 

# chat/cosumers.py
# 웹소캣 클라이언트와 1:1 통신

class EchoConsumer(WebsocketConsumer):
    def connect(self):
        # 들어오는 웹소캣 연결요청을 모두 허용한다.
        self.accept()
        
    def receive(self, text_data = None, bytes_data = None):
        # 웹소캣 클라이언트로부터의 텍스트/바이러니 메세지를 그대로 회신한다.
        self.send(text_data)
# chat/consumers.py
# 라이브블로그에 연결된 웹소캣 클라이언트에게 포스팅 생성/수정/삭제 시에 즉각 알림을 보내어
# 브라우저 새로고침없이도 웹 프론트엔드에서 즉각적으로 포스팅 추가/수정/삭제를 반영할 수 있도록 한다.

class LiveblogConsumer(WebsocketConsumer):
    # WebsocketConsumer에서는 self.groups에 지정된 그룹에 자동으로 add/discard를 수행한다.
    groups = ["liveblog"]
    
    def connect(self):
        # 들어오는 웹소캣 연결요청을 모두 허용한다.
        self.accept()
        
    # 그룹을 통해 받은 메세지를 그대로 웹소캣 클라이언트에게 전달한다.
    
    def liveblog_post_created(self, event_dict):
        post_id = event_dict["post_id"]
        self.send(json.dumps({
            "type" : "liveblog.post.created",
            "post_id" : post_id,
        })
        
    def liveblog_post_updated(self, event_dict):
        post_id = event_dict["post_id"]
        self.send(json.dumps({
            "type" : "liveblog.post.updated",
            "post_id" : post_id,
        })
        
    def liveblog_post_deleted(self, event_dict):
        post_id = event_dict["post_id"]
        self.send(json.dumps({
            "type" : "liveblog.post.deleted",
            "post_id" : post_id,
        })

 

 

Routing


View 함수/클래스도 요청 URL에 따라 그 요청을 처리할 View 함수를 결정할 수 있듯이 채널스에서는 세 가지 기준으로 현재 요청을 처리할 Consumer Instance를 결정할 수 있다. 

 

# chat/routing.py : 장고의 urls.py와 유사한 역할

from django.urls import path
from chat import consumers

websocket_urlpatterns = [
    # ws : //hostname/ws/echo/ 요청에 대응
    path("ws/echo/", consumers.EchoConsumer.as_asgi()),
    
    # ws : //hostname/ws/liveblog/ 요청에 대응
    path("ws/liveblog/", consumers.LiveblogConsumer.as_asgi()),
    
    # ws : //hostname/ws/chat/1234/chat/ 요청에 대응
    # URL을 통해 채팅방을 구별한다면?
    path("ws/chat/<int:room_pk>/chat/", consumers.ChatConsumer.as_asgi()),
]

# 워커 역할을 할 Consumer에 채널명(Channel name) 부여
worker_mapping = {
    "thumbnail-generate" : ThumbnailGenerateConsumer.as_asgi(),
}
# 프로젝트/asgi.py

from channels.routing import ProtocolTypeRouter, URLRouter, ChannelNameRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTING_MODULE", "프로젝트.settings")

# 프로젝트 초기화 과정을 수행
django_asgi_applicatioin = get_asgi_application()

import chat.routing

application = ProtocolTypeRouter( # ← 1단계) 프로토콜 타입에 의한 라우팅
    {
        # http 요청일 때 처리
        "http" : django_asgi_application,
        
        # websocket 요청일 때 처리
        "websocket" : URLRouter( # ← 2.1단계) 요청 URL에 의한 라우팅
            chat.routing.websocket_urlpatterns
        ),
        
        # worker 요청일 때 처리
        "channel" : ChannelNameRouter(
            chat.routing.worker_mapping # ← 2.2단계) 채널명에 의한 라우팅
            # | other_workers
        ),
    }
)

첫 번째 기준으로 프로토콜 타입으로, HTTP 프로토콜 요청과 WebSocket 프로토콜 요청을 구별한다. 이때 ProtocolTypeRouter를 활용하게 된다. 두 번째 기준으로는 요청 URL 문자열을 분기한다. URLRouter를 활용하며, 구현하는 대다수의 Consumer 클래스는 개별 URL을 가지고, URLRouter에 등록하게 될 것이다. 세 번째 기준으로는 채널명에 의한 라우팅이다. 이는 channels worker에서 사용한다. 

 

Cookies/Session/Auth


맨 처음 잠깐 언급하였지만, 채널수에서도 쿠키/세션/인증 기능을 활용할 수 있다. 장고 웹페이지에서의 쿠키/세션을 그대로 Consumer Instance에서도 활용할 수 있는 것이다. 예를 들어, Login View를 통해 인증된 유저가 웹 소캣으로 접속하면,  인증된 유저의 User Instance를 scope["user"]를 통해 조회할 수 있으며, 이 User Instance를 통해 인증여부(is_authenticated)를 알 수 있고, 웹 소캣 내에서 로그인/로그아웃 등의 기능도 구현할 수 있다. 

# chat/consumers.py

class ChatConsumer(JsonWebsocketConsumer):
    def connect(self):
        self.scope["cookies"]
        self.scope["session"]
        self.scope["user"]
        
        user = self.scope["user"]
        if not user.is_authenticated:
            pass

 

 

마무리


 

거북이는 언제나 거기에 있어

 

속담으로 "거북이는 언제나 거기에 있어"라는 세계 거북의 신화가 있다. 가장 큰 거북이가 아래에 있고, 그 위로 거북이가 층층이 쌓여있다. 마찬가지로 채널스에서는 요청을 처리하는 첫 ASGI application 함수가 있고,  그 안에, 안에, 안에 여러 ASGI application들을 랩핑 하는 구조로 동작한다. 모든 ASGI application 함수는 scope, receive, send 인자를 가지고 있다. 결국, ProtocolTypeRouter, AuthMiddlewareStack, URLRouter, ChannelNameRouter 등도 모두 ASGI application 함수인 것이다. 

# ASGI application의 기본 구조
async def application(scope, receive, send):
    event = await receive()
    ...
    await send({"type" : "websocket.send", ...})
    
# 프로젝트/asgi.py : 중첩된 ASGI applications에게 scope/receive/send 인자 전달
application = ProtocolTypeRouter(
    {
        "http" : get_asgi_application(),
        "websocket" : AuthMiddlewareStack(
            URLRouter(chat.routing.websocket_urlpatterns)
        ),
        "channel" : ChannelNameRouter(
            {
                "turtle" : TurtleConsumer.as_asgi(),
            }
        ),
    }
)

 

728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.