새소식

반응형
Toy Project/Django Web Chatting

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

  • -
반응형

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


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

 

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

 

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

 


 

현재 요청의 세부내역이 담긴 사전(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()), ]

 

 


# 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의 주요 구성요소에 대해 한 번 살펴보자. 

 


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, })

 

 


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에서 사용한다. 

 


맨 처음 잠깐 언급하였지만, 채널수에서도 쿠키/세션/인증 기능을 활용할 수 있다. 장고 웹페이지에서의 쿠키/세션을 그대로 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
반응형

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

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