백엔드 Back-end/장고 Django

Q. 장고 어드민Django admin에서 큰 크기의 csv 또는 엑셀 파일 내려받기를 구현할 수 있을까?

Tap to restart 2023. 5. 14. 11:00
반응형

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 함수가 처리되지 않는다.

 

 

반응형