일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- django erp
- django drf
- pip 오류
- Python
- tensorflow
- 재고 관리
- test drive development
- query 최적화
- pyside6 ui
- 채널스
- optimization page
- materialized
- django
- pyside6
- ERP
- qpa_plugin
- uiload
- django rank
- 장고로 ERP
- 장고
- QApplication
- django test
- pip 설치
- qwindows.dll
- Self ERP
- channels
- 파이썬
- 페이지 최적화
- bzpopmin
- 중량 관리
- Today
- Total
취미삼아 배우는 프로그래밍
Django Channels 사용한 퀴즈 페이지 만들기. 본문
1. 세팅
이번에 새로 시작하는 프로젝트가 생겼는데, 장고를 사용할 예정이고 Channels를 곁들여서 실시간기능을 넣고자 합니다.
제일먼저 해야할건 가상환경부터 설정해야겠죠
python -m venv myvenv
.\myvenv\Scripts\activate
python -m pip install -U pip
pip install django, channels, daphne, channels-redis
우선 가상환경 설치 및 기본적인 라이브러리 설치까지 해봅니다.
여기에서 daphne를 설치하는 이유는,
python manage.py runserver 를 통해서 테스트용 서버를 구동할 때 daphne를 사용하여 테스트용 서버를 파일 변동시마다 계속 열 수 있게끔 정식 지원하기 때문입니다.
그 다음은 장고 프로젝트 생성 및 앱 설정까지 해줄건데요.
django-admin startproject config .
django-admin startapp quizshow
그다음
설정을 만져줍니다.
[config/settings.py]
Installed_apps에 다프네, 채널스, 생성한 앱 까지 넣어줍니다.
그다음, WSGI_APPLICATION을 주석처리하고
# WSGI_APPLICATION = 'config.wsgi.application'
ASGI_APPLICATION = 'config.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
ASGI APPLICTAION을 지정해준뒤,
채널 레이어 방식도 지정해줍니다.
채널 레이어를 사용하는 방식에는 인메모리(InMemory), 레디스(Redis), 래빗MQ(RabbitMQ)등이 있는데,
레디스를 사용하는게 Channels 공식 사이트의 튜토리얼에 있다보니, 이걸 하는게 더 편하실겁니다.
그리고 나중에 더 하기 귀찮으니 그런갑다 하고 사용하는 코드
STATIC 지정도 맨 아래쪽에 추가해줍니다.
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'quizshow' / 'static'
]
이후
커맨드 라인을 통해서 기본적인 마이그레이션과 슈퍼계정을 생성해 줍니다.
python manage.py migrate
python manage.py createsuperuser
[id...]
[pw...]
[pw...]
그리고 레디스 윈도우용 파일이 필요합니다.(저는 윈도우 유저니까요)
레디스 사용에서도 유의해야할 점이 레디스가 버전이 너무 높으면 안됩니다.(현재는 7버전대..)
윈도우의 경우에는 5버전이 최신 버전이여서 별다른 선택지가 없지만, 리눅스 유저들의 경우에는 골치아플 수 있습니다.
윈도우 분들은 이걸 설치합시다.
https://github.com/tporadowski/redis/releases
Releases · tporadowski/redis
Native port of Redis for Windows. Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Se...
github.com
왜냐면, 공홈에서 5버전 이하를 사용하라고 하더라구요. 아니 3달전에도 업데이트하던 양반들이 이건 안해주네요..
딱 요렇게까지하면 기본적인 세팅이 끝납니다.
2. 모델 작성
모델은 나중에 바꾸거나 추가할 때 데이터베이스를 뒤엎어야할 수 도 있어서 조금 신중하게 해야하는 부분이긴 합니다. 그치만 반대로 말하면 뒤엎어도 될 데이터베이스가 별로 중요치 않다면..?
언제든지 뒤엎어도 상관없다는 거겠죠..?ㅎㅎ
모델 구조는 뒤로하고 일단 그냥 대충 적어줍니다.
from django.db import models
# Create your models here.
class Answers(models.Model):
content = models.CharField(verbose_name="답안", max_length=100)
def __str__(self):
return self.content
class Quiz(models.Model):
title = models.CharField(verbose_name="퀴즈 제목", max_length=80)
content = models.TextField(verbose_name="퀴즈 내용")
the_answers = models.ManyToManyField(Answers)
true_answer = models.ForeignKey(Answers, on_delete=models.CASCADE, related_name='true_answer')
def __str__(self):
return str(self.id) + " " + self.title
class QuizCurrentStatus(models.Model):
quiz_number = models.IntegerField(verbose_name="퀴즈 번호", default=0)
def __str__(self):
return str(self.id) + ' 퀴즈번호' + str(self.quiz_number)
사실, 대충 적은거 치고는 상당히 알차게 적었습니다
3. 템플릿 작성
템플릿을 작성하는 방식에는 여러가지가 있겠지만, 저의 경우에는 base.html을 만들어 extends 태그를사용해 베이스 파일을 상속받아 만드는 방식을 선호합니다. 그리고 상황에 따라 html문서를 include하는 방식도 사용하기도 하고, 뭔가 좀 템플릿 언어가 복잡하다 싶으면, Custom Template tag를 만들어서 파이썬으로 문서에 그냥 때려 박기도 합니다. 실제로 장고의 템플릿언어는 전용 템플릿 언어를 사용하는데 if문이 3~4개씩 속으로 들어가는 순간부터는 뭔말인지 알아보기 참 어렵습니다. 그럴 때는 그냥 Custom Template tag 로 파이썬 코드로 작성한 뒤 투입과 결과만을 생각해 두는게 훨씬 더 간편할 때가 많았습니다.
아무런 템플릿 경로 설정 없이 사용하고자 한다면, 다소 특이한 폴더구조를 기억해두시면 편합니다. 그냥 폴더를 지정해주셔도 좋구요.
저기 DIRS에 원하는 경로를 넣어주면 잘 챙겨줍니다.
DIRS에 굳이 폴더를 넣는걸 원치않으시다면, 그냥 구조대로 넣어서 사용하셔도 됩니다. 잘모르겠더라도 어차피 오류페이지에서
요기 파일 없는데요..? 요기랑 조기도 찾아봐는데 없던데요..?
라고 알려줍니다.
quizshow > templates > base.html
quizshow > templates > quizshow > main_page.html
앱 디렉토리에서 templates라는 폴더안의 파일을 우선해서 찾습니다.
그러면 base.html을 작성해볼까요
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script> -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script> -->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" >
<script src="{% static 'js/bootstrap.bundle.js' %}"></script>
<title>Test!</title>
{% block additional_style %}
<style></style>
{% endblock additional_style %}
</head>
<body>
{% block body %}
{% endblock body %}
</body>
{% block script %}
<script>
</script>
{% endblock script %}
</html>
load static은 스태틱 파일을 불러오겠다고 선언하는 것이고,
block들을 중간중간에 배치하여, extends한 페이지에서 사용할 구역을 미리 지정해두었습니다. 보시면, 별게 없는게 body, scrip, addtional_style블럭을 나누어 주었고, static폴더에 넣어둔 bootstrap 스태틱 파일을 불러오는거까지 돼있습니다.
https://getbootstrap.com/docs/5.3/getting-started/download/
Download
Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.
getbootstrap.com
여기에서 다운받아서
static폴더에 넣어두었습니다.
main_page.html은
문제 타이틀
문제내용문제내용문제내용문제내용문제내용문제내용문제내용문제내용문제내용
저는 이미 작성을 다 한 결과물을 올리는거인데,,(과정을 적는게 매우 길어질거같아서 그냥 넣었습니다.)
코드중 특별히 봐야할 부분은 재연결 시도를 하게하는 코드입니다.
그중에서도
connect라는 함수를 불러오면 자동으로 전역변수로 지정된 ws에 접속한 socket을 할당하는데, 만약 연결이 종료되 onclose가 발생했을 때 다시 connect함수를 불러와서 전역변수 ws에 다시 소켓연결을 시도한다는 내용의 코드가 되겠습니다.
코드를 작성해감에 있어서 이게 생각보다 유용합니다. python manage.py runserver를 통해 서버를 구동하게 될 경우 저장한번 할 때마다 소켓 연결이 끊어지는데, 이걸 다시 알아서 연결시켜주는 고마운 코드입니다.
4. 뷰 작성
뷰는 진짜 별게 없습니다. 사이트의 복잡도가 증가해지며 빠방하게 살이붙는 부분이 뷰 부분인데, 아직 기능적인 부분들만 구현할 거기 때문에 살이 안붙어서 뷰 부분은 별게 없습니다.
from django.shortcuts import render
# Create your views here.
def index_view(request):
template_name = "quizshow/main_page.html"
context = {}
return render(request, template_name, context)
5. urls.py 작성
뷰를 만들어 줬으면, url도 할당해주어야합니다.
config > urls.py
from django.contrib import admin
from django.urls import path
from quizshow.views import *
urlpatterns = [
path('admin/', admin.site.urls),
path('', index_view, name="index_page"),
]
만든 페이지가 별로 없으니, 진짜 별게 없습니다.
6. consumers.py작성
import json
from asgiref.sync import async_to_sync, sync_to_async
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer
from .models import QuizCurrentStatus
from .quiz_page_util import *
class QuizConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room = 'QuizRoom'
# sync_to_async(self.scope['session'].get('quiz_number', 1))()
# print(self.scope['session']['quiz_number'])
# self.current_quiz = self.get_quiz_number()
await self.channel_layer.group_add(self.room, self.channel_name)
await self.accept()
current_quiz_number = await self.get_quiz_number()
await self.channel_layer.group_send(
self.room, {'type':'move.page', 'message':current_quiz_number}
)
async def disconnect(self, event):
await self.channel_layer.group_discard(self.room, self.channel_name)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
action = text_data_json['action']
message = text_data_json['message']
if(action == 'move'):
await self.channel_layer.group_send(
self.room, {'type':'move.page', 'message':message}
)
if(action == 'click_answer'):
print(await self.get_clicked_answer(int(message)))
# await self.channel_layer.group_send(
# self.room, {"type":"chat.message", "message":message}
# )
async def move_page(self, event):
# number = await self.get_quiz_number()
number = event['message']
page_html = await get_quiz(number)
await self.set_quiz_number(number)
await self.send(text_data=json.dumps({'action':'move', 'html':page_html}))
async def click_answer(self, event):
clicked = await self.get_clicked_answer(int(event['message']))
print(clicked)
async def chat_message(self, event):
number = await self.get_quiz_number()
message = event['message'] + str(number)
# await self.plus_quiz_number()
await self.send(text_data=json.dumps({'message':message}))
@sync_to_async
def get_clicked_answer(self, number):
answer = Answers.objects.get(id=number)
return answer
@sync_to_async
def get_quiz_number(self):
q, created = QuizCurrentStatus.objects.get_or_create(id=1)
return q.quiz_number
@sync_to_async
def set_quiz_number(self, number):
q, created = QuizCurrentStatus.objects.get_or_create(id=1)
q.quiz_number = number
q.save()
# print(q.quiz_number)
return q.quiz_number
여기엔 조금 할 얘기가 많은데..
필요한 행위중 하나가 행사 진행자가 퀴즈쇼를 컨트롤 해야 하는 부분을 구현하는 거였습니다.
상식적으로 생각해도 행사 진행자가
자 모두 다음문제 버튼을 눌러주시죠!!
할 수는 없는 노릇이니까요.
적절히 정답버튼을 누른 다음 행사진행자의 진행에 따라 버튼을 누르고, 정답을 제한하고 풀고 등을 할 수 있는게 요건이었습니다.
그래서 그렇게 진행하려고 글로벌 변수를 활용해 현재의 퀴즈 번호를 반환하려 했었으나, 문제가 있었습니다.
각각의 페이지 접속자들의 클래스 변수로 자리잡고있었어서 진행자의 변수값을 바꾼다고 한들 전체의 변수값을 바꿀 수는 없었습니다.
그래서 어쩔 수 없이 현재의 퀴즈쇼 라는 모델을 작성하고 매 퀴즈마다 현재 퀴즈는 이걸 하고 있다고 덮어씌우는 방식을 채용하게 됐습니다.
7. locust 테스트 파일 작성
기껏 다 만들어놓고 실제 상황에서 사용자가 너무 많아 사용하지 못하게 된다면 이거는 진짜 큰 사고입니다.
그래서 이번엔 locust 테스트파일까지 작성해봤는데
스크린샷을 준비못했습니다. ㅠ
from locust import HttpUser, TaskSet, task, between
from websocket import create_connection
from locust import events
import json, time, random
import asyncio
# from quizshow.models import Answers
class ChatTaskSet(TaskSet):
def on_start(self):
self.uri = 'ws://127.0.0.1:8000/ws/'
self.ws = create_connection(self.uri)
# self.answer_len = len(Answers.objects.all())
print('start')
def send_messages(self, ws, body):
self.ws.send(body)
def on_quit(self):
self.ws.close()
print('stop')
def send_data(self, body):
self.ws.send(body)
# @task
# def send_move(self):
# # start_at = time.time()
# rand_num = random.randint(0, 2)
# body = json.dumps({'action': 'move', 'message': rand_num})
# self.ws.send(body)
# time.sleep(0.1)
@task
def send_pick(self):
rand_num = random.randint(1, 10)
body = json.dumps({'action':'click_answer', 'message':rand_num})
self.ws.send(body)
time.sleep(0.1)
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print('A new test is starting')
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print('A new test is ending')
class WebsiteUser(HttpUser):
host = '127.0.0.1'
tasks = [ChatTaskSet]
# task_set = ChatTaskSet
wait_time = between(1, 10)
# locust -f .\test_locust.py
여기에서 웹소켓 테스트를 하기위해 설치해야하는 라이브러리가
pip install websocket-client
로 해야 저 함수를 쓸 수 있다는걸 2일만에 알았다는걸 제외하면 만들기 어렵진 않았습니다.
여기에서 테스트를 너무 고속으로 하다보면 웹소켓이 64개이상 안된다고 떠버리던데 이거는 어떻게 해결할 방법이 있는지는 잘 모르겠네요
추후에 다른 테스트들을 통해서 개선해 가봐야겠습니다.
8. 리액트로의 이전
그러던 와중 친구의 푸념을 듣게됩니다.
이전에 만들었었던 프로젝트에서 행사장이 사람이 너무 몰리면 인터넷이 안되서 그게 걱정된다. 그래서 저번에 했던것들 중에서는 인터넷 문제로 페이지가 늦게 뜨는게 싫었다.
라구요. 리액트가 그렇다고 느린 인터넷 환경을 빠르게 만들어주지는 않지만, 리액트를 이용한 Single Page App(SPA)는 전체의 로딩만 된다면 크게 문제가 없을것으로 판단됐습니다. 단, Vue.js Angular.js등의 자바스크립트 프레임워크도 많은데, 아무래도 보다는 갖고있는 기술 스펙 내에서는 리액트가 더 현실적이지 않겠나 판단됐습니다.
그래서 하루는 리액트를 이용한 영단어 페이지 만들기
https://www.youtube.com/watch?v=05uFo_-SGXU&list=PLZKTXPmaJk8J_fHAzPLH8CJ_HO_M33e7-
2일정도는 리액트를 이용한 채팅 앱 만들기
https://www.youtube.com/watch?v=uE9Ncr6qInQ
예제를 학습했고,, 할만하긴 하겠다. 판단했습니다.
리액트를 하는 이유가 하나 더 있는데, 예전에 친구가 납기일 3일전에 갑자기..
야 근데 이거 중간에 페이지가 흰페이지 보이는데 이건 어떻게 안되냐..?
해가지고 임시 방편으로나마 리액트를 따라하기 위해 전체로딩 후 자바스크립트로 캐시된 이미지 교체, 캐시에 올리기 위해 lazy-loading 등도 미리 다 쓰고 별짓을 다했었는데 결국 줄긴 줄었다지만 친구가 말하는 그 0.1~2초 정도의 간극은 어찌할 도리가 없었습니다. 고객님인데.. 닥치고 해달라는데로 해야죠뭐..
한 가지 걸리는 점은 뒤 채팅 앱은
react를 쓰는건 맞으나, 웹소켓 서버를 node js의 socket.io를 통해 통신하기에 장고 channels의 규격과는 맞지 않아서 생판 초짜인 상태에서 node 소켓 서버를 만들것인가 하는 문제를 직결하게 됩니다.
https://velog.io/@qkrdudgh052/Reat-and-Django-Channels-Websocket
React and Django Channels Websocket
React에서 WebSocket을 사용할때 대부분의 라이브러리로는 Socket.io 라이브러리를 사용하는걸로 알고 있었다. Socket.io를 쓰면 Node.js와 궁합이 잘맞기 때문에 사용함에 있어 불편함이 없는걸로 알고 있
velog.io
해당 글에서는 리액트 프론트-장고채널스 웹소켓 서버를 활용하는 걸 보여주고 있는데
그냥 다른 라이브러리는 쓰지않고 장고의 html처럼 기본 내장된 WebSocket을 사용합니다.
뭐 이거도 나쁘지만은 않은게 기존 코드와 거의 차이가 없을것이니 딱히 나쁜 일은 아닌거같습니다.
'파이썬(장고)' 카테고리의 다른 글
고놈의 Channels / Channels_redis / BZPOPMIN (0) | 2025.02.15 |
---|---|
Django로 개인 업무(ERP) 홈페이지 만들기-7 (11) | 2020.11.22 |
Django로 개인 업무(ERP) 홈페이지 만들기-6 (0) | 2020.11.16 |
Django로 개인 업무(ERP) 홈페이지 만들기-5 (0) | 2020.11.14 |
Django로 개인 업무(ERP) 홈페이지 만들기-4 (0) | 2020.11.13 |