[Django] 장고 웹 채팅 서비스 - Channels 주요 구성 요소
- -
장고 웹 채팅 서비스 - 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(),
}
),
}
)
'Toy Project > Django Web Chatting' 카테고리의 다른 글
장고 웹 채팅 서비스(실습) - 초간단 Echo 웹 소켓 구현 (0) | 2024.03.23 |
---|---|
[Django] 장고 웹 채팅 서비스(1) - 프로젝트 생성 (1) | 2024.01.07 |
소중한 공감 감사합니다