A. celery를 활용해 비동기 처리하면 구현할 수 있다.
장고 어드민 사이트The Django admin site란?
장고에서 기본 제공해주는 관리자 화면이다. 웹페이지가 기본 구현되어 있어서 몇가지 어드민 관련 설정만 해주면 되어서 편하다.
아래 예가 그 예이다.

사용자 등 장고 모델과 연관되어 데이터 추가 삭제 수정을 아주 쉽게 관리할 수 있다.
초기 스타트업에서 장고로 서비스를 구현했을 때 보통 백오피스(back office)라고 많이 부르는 내부 직원용 페이지를 굳이 따로 만들지 않고 장고 어드민을 많이 활용한다.
큰 크기의 csv나 엑셀 파일을 내려받는 경우와 관련 이슈
데이터베이스 접근은 백엔드 개발자 등 일부만 가능하다. 따라서 분석 등을 위해서 데이터를 csv 파일이나 엑셀 파일로 받을 필요가 발생한다. 이때 데이터가 굉장히 클 경우 문제가 발생한다. 바로 타임아웃이다. 예를 들어서 데이터가 100만건이라고 하자. 이 데이터 100만건을 csv나 엑셀 파일로 만드는 작업 자체가 굉장히 오래 걸리게 된다. 이 때 타임아웃이 발생하고 HTTP 연결이 끊어진다.
celery를 활용한 비동기 처리
이때 celery를 활용해서 비동기 처리로 이슈를 해결할 수 있다.
엑셀을 생성하는 download_xlsx 함수를 비동기로 실행해서 task 객체를 얻고, task가 성공했는지 장고 어드민 페이지에서 1초 간격으로 확인한 뒤 성공했을 때 파일명을 반환하고, 해당 파일명으로 다운로드를 실행하는 식으로 구현할 수 있다.
task = download_xlsx.delay(slug)
위 코드처럼 celery task로 등록한 함수를 .delay로 실행하면 task 객체를 얻을 수 있다. task의 id 속성으로 AsyncResult(task_id) 객체를 만들면 status를 통해서 해당 태스크가 실행 중인지 종료되었는지 확인할 수 있다. status가 success이면 task 함수의 반환값을 result 속성에서 확인할 수 있다.
실제 구현 예는 아래와 같다.
admin.py 예
@admin.register(Submit)
class SubmitAdmin(admin.ModelAdmin):
...
change_list_template = "list.html"
...
def get_urls(self):
urls = [
path("download/", self.download, name="download"),
path("download-status/", self.download_status, name="download_status"),
path("download-file/", self.download_file, name="download_file"),
]
return urls + super().get_urls()
def download(self, request):
if not request.user.is_staff:
raise Http404()
slug = request.GET.get("form__slug")
task = download_xlsx.delay(slug)
return JsonResponse({"task": task.id}, status=status.HTTP_202_ACCEPTED)
def download_status(self, request):
if not request.user.is_staff:
raise Http404()
task = request.GET.get("task")
task_result = AsyncResult(task)
payload = {
"task": task,
"status": task_result.status,
"result": task_result.result,
}
return JsonResponse(payload, status=status.HTTP_200_OK)
def download_file(self, request):
if not request.user.is_staff:
raise Http404()
filename = request.GET.get("filename")
filepath = f"/tmp/forms/{filename}"
response = FileResponse(open(filepath, "rb"))
response["Content-Disposition"] = f"attachment; filename={filename}"
return response
list.html 예
{% extends 'admin/change_list.html' %}
{% block object-tools-items %}
<script>
let httpRequest = new XMLHttpRequest();
const getTask = function(){
const queryParams = window.location.search.slice(1);
if(queryParams.length === 0){
alert('Please select slug.')
}else{
httpRequest.onreadystatechange = getResponse;
httpRequest.open('GET', 'download/?' + queryParams);
httpRequest.send();
function getResponse() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
const task = JSON.parse(httpRequest.responseText).task;
checkStatus(task);
}
}
}
}
const checkStatus = function(task){
httpRequest.onreadystatechange = getResponse;
httpRequest.open('GET', 'download-status/?task=' + task);
httpRequest.send();
function getResponse() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
const res = JSON.parse(httpRequest.responseText);
if(res.status === 'SUCCESS'){
download(res.result);
}else{
setTimeout(() => {
checkStatus(res.task);
}, 1000);
}
}
}
}
const download = function(filename){
window.location.href = '/admin/forms/submit/download-file/?filename=' + filename;
}
</script>
<li><a href='#' onclick='getTask();'>Download</a></li>
{{ block.super }}
{% endblock %}
전체 코드는 forms 프로젝트에서 확인하고 실행해 볼 수 있다.
celery 반드시 따로 실행할 것
장고와 별개로 반드시 celery를 따로 실행해줘야 한다.
$ celery -A config worker -l info
따로 실행해주지 않으면 .delay 함수가 처리되지 않는다.