Django Channels 완전 정복: WebSocket과 실시간 통신 구현 가이드
실시간 채팅, 라이브 알림, 협업 도구 등을 개발하면서 Django의 기본 기능만으로는 한계를 느끼셨나요? Django Channels는 Django 애플리케이션에 WebSocket, HTTP/2, 그리고 다른 프로토콜을 지원하여 실시간 기능을 구현할 수 있게 해주는 강력한...
실시간 채팅, 라이브 알림, 협업 도구 등을 개발하면서 Django의 기본 기능만으로는 한계를 느끼셨나요? Django Channels는 Django 애플리케이션에 WebSocket, HTTP/2, 그리고 다른 프로토콜을 지원하여 실시간 기능을 구현할 수 있게 해주는 강력한 라이브러리입니다. 이 글에서는 Django Channels의 설치부터 실제 채팅 애플리케이션 구현까지 모든 과정을 상세히 다루겠습니다.
🚀 Django Channels란?
Django Channels의 핵심 개념
Django는 기본적으로 WSGI (Web Server Gateway Interface)를 사용하는 동기식 웹 프레임워크입니다. 하지만 실시간 기능을 위해서는 ASGI (Asynchronous Server Gateway Interface)가 필요합니다.
# 기존 Django (WSGI) - 동기식
def my_view(request):
# 요청 처리
return HttpResponse("Hello World")
# Django Channels (ASGI) - 비동기식
async def my_consumer(scope, receive, send):
# 비동기 요청 처리
await send({
'type': 'websocket.accept'
})
Django Channels의 주요 특징:
- 비동기 처리: WebSocket, HTTP/2 등 지원
- 채널 계층: 서버 간 메시지 전달
- Consumer: WebSocket 연결 처리
- Routing: URL 패턴과 Consumer 매핑
📦 설치 및 기본 설정
1. Django Channels 설치
# 가상환경 생성 및 활성화
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Django와 Channels 설치
pip install django
pip install channels
pip install channels-redis # Redis 채널 레이어용
2. Django 프로젝트 생성
# 새 프로젝트 생성
django-admin startproject chatproject
cd chatproject
# 앱 생성
python manage.py startapp chat
3. settings.py 설정
# chatproject/settings.py
import os
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels', # Channels 추가
'chat', # 생성한 앱 추가
]
# ASGI 애플리케이션 설정
ASGI_APPLICATION = 'chatproject.asgi.application'
# 채널 레이어 설정 (개발용 - 인메모리)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
# 프로덕션용 Redis 설정 (나중에 사용)
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
# 'CONFIG': {
# "hosts": [('127.0.0.1', 6379)],
# },
# },
# }
4. ASGI 설정 파일 생성
# chatproject/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatproject.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
🛠️ Consumer 작성
Consumer는 WebSocket 연결을 처리하는 핵심 컴포넌트입니다.
1. 기본 Consumer 구현
# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
"""WebSocket 연결 시 호출"""
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
# 그룹에 참가
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# WebSocket 연결 수락
await self.accept()
print(f"User connected to room: {self.room_name}")
async def disconnect(self, close_code):
"""WebSocket 연결 해제 시 호출"""
# 그룹에서 나가기
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
print(f"User disconnected from room: {self.room_name}")
async def receive(self, text_data):
"""클라이언트로부터 메시지 수신"""
try:
text_data_json = json.loads(text_data)
message = text_data_json['message']
username = text_data_json.get('username', 'Anonymous')
# 그룹의 모든 사용자에게 메시지 전송
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
except json.JSONDecodeError:
# 잘못된 JSON 형식의 메시지 처리
await self.send(text_data=json.dumps({
'error': 'Invalid message format'
}))
async def chat_message(self, event):
"""그룹으로부터 메시지를 받아 WebSocket으로 전송"""
message = event['message']
username = event['username']
# WebSocket으로 메시지 전송
await self.send(text_data=json.dumps({
'message': message,
'username': username,
'timestamp': self.get_timestamp()
}))
def get_timestamp(self):
"""현재 시간 반환"""
from datetime import datetime
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
2. 고급 Consumer 기능
# chat/consumers.py (고급 기능 추가)
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth.models import User
from .models import ChatRoom, Message
class AdvancedChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.user = self.scope['user']
# 인증된 사용자만 접근 허용
if self.user.is_anonymous:
await self.close()
return
# 채팅방 존재 확인 및 생성
self.room = await self.get_or_create_room(self.room_name)
# 그룹에 참가
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# 사용자 접속 알림
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_join',
'username': self.user.username
}
)
async def disconnect(self, close_code):
# 사용자 퇴장 알림
if hasattr(self, 'room_group_name') and hasattr(self, 'user'):
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_leave',
'username': self.user.username
}
)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
try:
data = json.loads(text_data)
message_type = data.get('type', 'message')
if message_type == 'message':
await self.handle_message(data)
elif message_type == 'typing':
await self.handle_typing(data)
except json.JSONDecodeError:
await self.send_error('Invalid JSON format')
async def handle_message(self, data):
"""메시지 처리"""
message_text = data.get('message', '').strip()
if not message_text:
return
# 데이터베이스에 메시지 저장
message_obj = await self.save_message(message_text)
# 그룹에 메시지 전송
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message_text,
'username': self.user.username,
'message_id': message_obj.id,
'timestamp': message_obj.created_at.isoformat()
}
)
async def handle_typing(self, data):
"""타이핑 상태 처리"""
is_typing = data.get('is_typing', False)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'typing_status',
'username': self.user.username,
'is_typing': is_typing
}
)
# 데이터베이스 작업을 위한 동기 함수들
@database_sync_to_async
def get_or_create_room(self, room_name):
from .models import ChatRoom
room, created = ChatRoom.objects.get_or_create(name=room_name)
return room
@database_sync_to_async
def save_message(self, message_text):
from .models import Message
return Message.objects.create(
room=self.room,
user=self.user,
content=message_text
)
# 이벤트 핸들러들
async def chat_message(self, event):
await self.send(text_data=json.dumps({
'type': 'message',
'message': event['message'],
'username': event['username'],
'message_id': event['message_id'],
'timestamp': event['timestamp']
}))
async def user_join(self, event):
await self.send(text_data=json.dumps({
'type': 'user_join',
'username': event['username']
}))
async def user_leave(self, event):
await self.send(text_data=json.dumps({
'type': 'user_leave',
'username': event['username']
}))
async def typing_status(self, event):
# 자신의 타이핑 상태는 전송하지 않음
if event['username'] != self.user.username:
await self.send(text_data=json.dumps({
'type': 'typing',
'username': event['username'],
'is_typing': event['is_typing']
}))
async def send_error(self, message):
await self.send(text_data=json.dumps({
'type': 'error',
'message': message
}))
🔗 Routing 설정
1. WebSocket URL 패턴
# chat/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
# 고급 Consumer 사용 시
# re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.AdvancedChatConsumer.as_asgi()),
]
2. HTTP URL 패턴
# chat/urls.py
from django.urls import path
from . import views
app_name = 'chat'
urlpatterns = [
path('', views.index, name='index'),
path('<str:room_name>/', views.room, name='room'),
]
# chatproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('chat/', include('chat.urls')),
]
📊 모델 설계
# chat/models.py
from django.db import models
from django.contrib.auth.models import User
class ChatRoom(models.Model):
name = models.CharField(max_length=100, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Meta:
ordering = ['-updated_at']
class Message(models.Model):
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='messages')
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user.username}: {self.content[:50]}'
class Meta:
ordering = ['created_at']
class OnlineUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE)
connected_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'room']
🖼️ 뷰와 템플릿 작성
1. 뷰 함수
# chat/views.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import ChatRoom, Message
def index(request):
"""채팅방 목록"""
rooms = ChatRoom.objects.all()
return render(request, 'chat/index.html', {'rooms': rooms})
@login_required
def room(request, room_name):
"""특정 채팅방"""
room_obj, created = ChatRoom.objects.get_or_create(name=room_name)
# 최근 메시지 50개 가져오기
messages = Message.objects.filter(room=room_obj).select_related('user').order_by('-created_at')[:50]
messages = reversed(messages)
context = {
'room_name': room_name,
'messages': messages,
'user': request.user
}
return render(request, 'chat/room.html', context)
2. 템플릿 작성
<!-- chat/templates/chat/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>{% raw %}{% block title %}Django Channels Chat{% endblock %}{% endraw %}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: #007bff;
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{% raw %}{% block header %}Django Channels Chat{% endblock %}{% endraw %}</h1>
</div>
<div class="content">
{% raw %}{% block content %}
{% endblock %}{% endraw %}
</div>
</div>
</body>
</html>
<!-- chat/templates/chat/room.html -->
{% raw %}{% extends 'chat/base.html' %}
{% block title %}Room: {{ room_name }}{% endblock %}
{% block header %}
채팅방: {{ room_name }}
<small style="display: block; margin-top: 10px;">
사용자: {{ user.username }}
</small>
{% endblock %}
{% block content %}{% endraw %}
<div id="chat-container">
<div id="chat-log" style="height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; margin-bottom: 10px; background: #f9f9f9;">
{% raw %}{% for message in messages %}
<div class="message" style="margin-bottom: 10px;">
<strong>{{ message.user.username }}</strong>
<span style="color: #666; font-size: 0.8em;">{{ message.created_at|date:"H:i:s" }}</span><br>
{{ message.content }}
</div>
{% endfor %}{% endraw %}
</div>
<div id="typing-indicator" style="color: #666; font-style: italic; height: 20px; margin-bottom: 10px;"></div>
<div style="display: flex; gap: 10px;">
<input id="chat-message-input" type="text"
placeholder="메시지를 입력하세요..."
style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<button id="chat-message-submit"
style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
전송
</button>
</div>
</div>
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const userName = JSON.parse(document.getElementById('user-name').textContent);
// WebSocket 연결
const chatSocket = new WebSocket(
'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
);
const chatLog = document.querySelector('#chat-log');
const messageInputDom = document.querySelector('#chat-message-input');
const submitButton = document.querySelector('#chat-message-submit');
const typingIndicator = document.querySelector('#typing-indicator');
let typingTimer;
let isTyping = false;
// WebSocket 메시지 수신
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
switch(data.type) {
case 'message':
addMessage(data.username, data.message, data.timestamp);
break;
case 'user_join':
showNotification(data.username + '님이 입장했습니다.');
break;
case 'user_leave':
showNotification(data.username + '님이 퇴장했습니다.');
break;
case 'typing':
handleTypingStatus(data.username, data.is_typing);
break;
case 'error':
showError(data.message);
break;
}
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
showError('연결이 끊어졌습니다. 페이지를 새로고침해주세요.');
};
// 메시지 전송
function sendMessage() {
const message = messageInputDom.value.trim();
if (message) {
chatSocket.send(JSON.stringify({
'type': 'message',
'message': message
}));
messageInputDom.value = '';
// 타이핑 상태 종료
if (isTyping) {
sendTypingStatus(false);
}
}
}
// 타이핑 상태 전송
function sendTypingStatus(typing) {
chatSocket.send(JSON.stringify({
'type': 'typing',
'is_typing': typing
}));
isTyping = typing;
}
// 메시지 추가
function addMessage(username, message, timestamp) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.style.marginBottom = '10px';
const time = new Date(timestamp).toLocaleTimeString('ko-KR');
messageDiv.innerHTML = `
<strong>${username}</strong>
<span style="color: #666; font-size: 0.8em;">${time}</span><br>
${message}
`;
chatLog.appendChild(messageDiv);
chatLog.scrollTop = chatLog.scrollHeight;
}
// 알림 표시
function showNotification(message) {
const notifDiv = document.createElement('div');
notifDiv.style.cssText = 'color: #666; font-style: italic; text-align: center; margin: 10px 0;';
notifDiv.textContent = message;
chatLog.appendChild(notifDiv);
chatLog.scrollTop = chatLog.scrollHeight;
}
// 에러 표시
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'color: red; font-weight: bold; text-align: center; margin: 10px 0;';
errorDiv.textContent = '에러: ' + message;
chatLog.appendChild(errorDiv);
chatLog.scrollTop = chatLog.scrollHeight;
}
// 타이핑 상태 처리
function handleTypingStatus(username, typing) {
if (typing) {
typingIndicator.textContent = username + '님이 입력 중...';
} else {
typingIndicator.textContent = '';
}
}
// 이벤트 리스너
submitButton.onclick = sendMessage;
messageInputDom.onkeyup = function(e) {
if (e.keyCode === 13) { // Enter
sendMessage();
} else {
// 타이핑 상태 관리
if (!isTyping) {
sendTypingStatus(true);
}
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
if (isTyping) {
sendTypingStatus(false);
}
}, 1000);
}
};
messageInputDom.focus();
</script>
{% raw %}{{ room_name|json_script:"room-name" }}
{{ user.username|json_script:"user-name" }}
{% endblock %}{% endraw %}
🔄 Redis를 이용한 프로덕션 설정
1. Redis 설치 및 설정
# Ubuntu/Debian
sudo apt-get install redis-server
# macOS
brew install redis
# Redis 서비스 시작
redis-server
# 연결 테스트
redis-cli ping
# PONG 응답이 나와야 함
2. 프로덕션 settings.py
# chatproject/settings.py
import os
# Redis 채널 레이어 설정
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
"capacity": 1500, # 채널당 최대 메시지 수
"expiry": 60, # 메시지 만료 시간 (초)
},
},
}
# 로깅 설정
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'channels.log',
},
},
'loggers': {
'channels': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
3. Docker를 이용한 Redis 설정
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
web:
build: .
ports:
- "8000:8000"
depends_on:
- redis
environment:
- REDIS_URL=redis://redis:6379
volumes:
redis_data:
🚀 배포 및 성능 최적화
1. Daphne를 이용한 프로덕션 서버
# Daphne 설치
pip install daphne
# 프로덕션 서버 실행
daphne -p 8000 chatproject.asgi:application
# 백그라운드 실행
nohup daphne -p 8000 chatproject.asgi:application &
2. Nginx 설정
# /etc/nginx/sites-available/chatproject
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
3. 성능 최적화 팁
# chat/consumers.py - 최적화된 Consumer
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.core.cache import cache
class OptimizedChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.user = self.scope['user']
if self.user.is_anonymous:
await self.close()
return
# 연결된 사용자 수 증가
await self.increment_user_count()
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
if hasattr(self, 'room_group_name'):
# 연결된 사용자 수 감소
await self.decrement_user_count()
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
try:
data = json.loads(text_data)
# 메시지 길이 제한
if len(data.get('message', '')) > 1000:
await self.send_error('메시지가 너무 깁니다.')
return
# 스팸 방지 - 사용자당 초당 메시지 제한
if not await self.check_rate_limit():
await self.send_error('메시지 전송 속도가 너무 빠릅니다.')
return
await self.handle_message(data)
except json.JSONDecodeError:
await self.send_error('잘못된 메시지 형식입니다.')
async def check_rate_limit(self):
"""초당 메시지 제한 확인"""
cache_key = f'rate_limit_{self.user.id}'
current_count = cache.get(cache_key, 0)
if current_count >= 5: # 초당 5개 메시지 제한
return False
cache.set(cache_key, current_count + 1, timeout=1)
return True
async def increment_user_count(self):
"""방의 사용자 수 증가"""
cache_key = f'room_users_{self.room_name}'
current_count = cache.get(cache_key, 0)
cache.set(cache_key, current_count + 1, timeout=3600)
async def decrement_user_count(self):
"""방의 사용자 수 감소"""
cache_key = f'room_users_{self.room_name}'
current_count = cache.get(cache_key, 1)
cache.set(cache_key, max(0, current_count - 1), timeout=3600)
# ... 나머지 메서드들
🐛 디버깅 및 테스트
1. 테스트 작성
# chat/tests.py
from channels.testing import WebsocketCommunicator
from channels.db import database_sync_to_async
from django.test import TransactionTestCase
from django.contrib.auth.models import User
from .consumers import ChatConsumer
class ChatConsumerTest(TransactionTestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
async def test_chat_consumer_connection(self):
"""WebSocket 연결 테스트"""
communicator = WebsocketCommunicator(
ChatConsumer.as_asgi(),
"/ws/chat/test/"
)
communicator.scope['user'] = self.user
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
await communicator.disconnect()
async def test_chat_message_sending(self):
"""메시지 전송 테스트"""
communicator = WebsocketCommunicator(
ChatConsumer.as_asgi(),
"/ws/chat/test/"
)
communicator.scope['user'] = self.user
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
# 메시지 전송
await communicator.send_json_to({
'type': 'message',
'message': 'Hello World!'
})
# 메시지 수신 확인
response = await communicator.receive_json_from()
self.assertEqual(response['message'], 'Hello World!')
self.assertEqual(response['username'], 'testuser')
await communicator.disconnect()
2. 디버깅 팁
# 로깅 설정으로 디버깅
import logging
logger = logging.getLogger(__name__)
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
logger.info(f"User {self.scope['user']} connecting to {self.room_name}")
# ... 연결 로직
async def receive(self, text_data):
logger.debug(f"Received message: {text_data}")
# ... 메시지 처리 로직
async def disconnect(self, close_code):
logger.info(f"User {self.scope['user']} disconnected with code {close_code}")
# ... 연결 해제 로직
🔒 보안 고려사항
1. 인증 및 권한 확인
# chat/consumers.py
from channels.auth import login_required
from channels.generic.websocket import AsyncWebsocketConsumer
class SecureChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
# 인증된 사용자만 허용
if self.scope['user'].is_anonymous:
await self.close(code=4001)
return
# 방 접근 권한 확인
if not await self.has_room_permission():
await self.close(code=4003)
return
# ... 연결 로직
@database_sync_to_async
def has_room_permission(self):
"""방 접근 권한 확인"""
# 비즈니스 로직에 따라 구현
return True
2. 메시지 검증 및 필터링
import re
from django.utils.html import escape
class SecureChatConsumer(AsyncWebsocketConsumer):
BANNED_WORDS = ['spam', 'abuse'] # 금지어 목록
def sanitize_message(self, message):
"""메시지 검증 및 정제"""
# HTML 이스케이프
message = escape(message)
# 길이 제한
if len(message) > 500:
return None
# 금지어 필터링
for word in self.BANNED_WORDS:
if word.lower() in message.lower():
return None
return message.strip()
📈 모니터링 및 분석
1. 메트릭 수집
# chat/middleware.py
import time
from django.core.cache import cache
class WebSocketMetricsMiddleware:
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
if scope["type"] == "websocket":
start_time = time.time()
# 연결 수 증가
cache.set('websocket_connections',
cache.get('websocket_connections', 0) + 1,
timeout=3600)
try:
return await self.inner(scope, receive, send)
finally:
# 연결 시간 기록
duration = time.time() - start_time
cache.set('avg_connection_duration',
(cache.get('avg_connection_duration', 0) + duration) / 2,
timeout=3600)
# 연결 수 감소
cache.set('websocket_connections',
max(0, cache.get('websocket_connections', 1) - 1),
timeout=3600)
else:
return await self.inner(scope, receive, send)
2. 관리자 대시보드
# chat/admin.py
from django.contrib import admin
from django.urls import path
from django.shortcuts import render
from django.core.cache import cache
from .models import ChatRoom, Message
@admin.register(ChatRoom)
class ChatRoomAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at', 'message_count', 'active_users']
readonly_fields = ['created_at', 'updated_at']
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('metrics/', self.admin_site.admin_view(self.metrics_view),
name='chat_metrics'),
]
return custom_urls + urls
def metrics_view(self, request):
context = {
'websocket_connections': cache.get('websocket_connections', 0),
'avg_connection_duration': cache.get('avg_connection_duration', 0),
'total_rooms': ChatRoom.objects.count(),
'total_messages': Message.objects.count(),
}
return render(request, 'admin/chat_metrics.html', context)
def message_count(self, obj):
return obj.messages.count()
def active_users(self, obj):
return cache.get(f'room_users_{obj.name}', 0)
🎯 결론
Django Channels는 Django 애플리케이션에 실시간 기능을 추가할 수 있는 강력한 도구입니다. 이 가이드에서 다룬 내용들을 정리하면:
핵심 포인트
- ASGI 기반: WebSocket과 HTTP/2 지원
- Consumer: WebSocket 연결 처리의 핵심
- 채널 레이어: 서버 간 메시지 전달
- Redis: 프로덕션 환경에서의 필수 구성요소
다음 단계
- 확장 기능: 파일 전송, 화상/음성 채팅
- 모바일 앱: WebSocket을 이용한 모바일 연동
- 마이크로서비스: 분산 환경에서의 Channels 활용
Django Channels를 통해 현대적인 실시간 웹 애플리케이션을 구축해보세요. 사용자 경험을 크게 향상시킬 수 있는 강력한 도구가 될 것입니다.