웹 프로그래밍

[Django & Ajax] 실시간 채팅 구현

728x90
반응형

남들이 해놓은걸 보고 만들어볼 계획이었는데 주어진 시간에 비해 너무 어렵다. 조잡해질까봐 걱정이 되긴 하나 그렇게 복잡하지 않을 것 같아 그냥 직접 구현해보려한다.

 

어떻게 구현해야할까에 대한 고민

기왕이면 그나마 친숙한 Ajax를 이용하려한다. 대략적인 생각은 다음과 같다. ajax로 채팅내용을 뿌려주는 함수를 구현해놓고 일정주기로 함수를 호출해서 화면을 재구성하는 방식이다. 일정주기로 계속 호출을 한다는게 마음에 걸려서 다른 방식이 없을까 조사를 해봤는데 웹소켓말고 ajax를 이용하는 방식내에서는 어쩔 수 없는거같다. 

 

setInterval()

https://developer.mozilla.org/en-US/docs/Web/API/setInterval

 

setInterval() - Web APIs | MDN

The setInterval() method, offered on the Window and Worker interfaces, repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.

developer.mozilla.org

 일정주기로 함수를 실행시키는 건 setInterval()을 이용하면 되는 듯하다.

 

views.py

class ChatView(View):
    def get(self, request):
        message = LandingChat.objects.all().order_by('date')
        return render(request,'chatapp/landing_chat.html', {'message': message})

    def post(self, request):
        if request.is_ajax():
            model = LandingChat()
            model.chat = request.POST.get('message')
            model.writer = request.user
            model.save()
            return JsonResponse({'message': model.chat})

 

ajax.js

//landing-chat 채팅화면렌더링함수
function chatRender() {
    $.ajax({
        type: "GET",
        url: "/chat/",
        data: {"message": $('#chat-box').val()},
        success: function(res) {
            var e = $(res).find('#chat-box').children();
            $('#chat-box').children().remove();
            $('#chat-box').html(e);
            $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
        }
    });
}
$(document).ready(function(){
	//landing-chat
    //엔터감지
    $('#landing-chat-input').keydown(function(key){
        if(key.keyCode == 13){
            $('#landing-chat-send').click();
        }
    });
    //스크롤 항상맨아래
    $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
    //3초마다 채팅렌더링
    setInterval(chatRender, 3000);
    $('#landing-chat-send').on("click", (e) => {
        message = $('#landing-input-area').val();
        $.ajax({
            type: "POST",
            url: '/chat/',
            data: {"message": message},
            dataType: "json",
            beforeSend: function(xhr) {
                xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
            },
            success: function (res) {
                $('landing-input-area').text('');
                chatRender();

            },
            error: function (res) {
                alert("전송실패. 지속될 경우 에러제보페이지에 문의바랍니다.")
            }
        });
    });
});

 

대충 만들었는데 우선 작동은 한다. 


만들어놓고보니 서비스에 포함시키려면 좀 고쳐야겠다는 생각이 들었다. 자원낭비를 좀더 줄일 수 있을 것 같다. 현재는 chatRender()를 계속 호출하는 방식인데 setInterval()에서 이걸 계속 호출할 것이 아니라 db변경사항을 탐색하는 함수를 만들어 계속 감시하고 변경사항이 있다면 그때 chatRender()를 호출하면 개선될 거라는 생각이다. 구현후 이어 작성하겠다.

 

db감시를 어떻게 해야할까에 대한 고민

헛짓을 좀 하면서 몇가지를 생각해봤는데 시간을 이용하는게 좋을것같다.

우선 뷰에서 jsonresponse로 마지막으로 저장된 시간요소를 보내고 ajax로 그걸 받는 함수를 만들거다.

그리고 setInterval()로 그걸 계속 받는다. 그리고 모르겠다 해보고 이어가겠다.


views.py

class ChatView(View):

    def get(self, request):
        message = LandingChat.objects.all().order_by('date')
        return render(request, 'chatapp/landing_chat.html', {'message': message})


    def post(self, request):
        if request.is_ajax():
            dbtrigger = int(request.POST.get('dbtrigger', 0))
            if (dbtrigger):
                model = LandingChat()
                last_msg = LandingChat.objects.latest('date').date
                return JsonResponse({'message': model.chat, 'last_msg': last_msg})
            else:
                model = LandingChat()
                model.chat = request.POST.get('message')
                model.writer = request.user
                model.save()
                if LandingChat.objects.all().count()>50:
                    LandingChat.objects.earliest('date').delete()
                last_msg = LandingChat.objects.latest('date').date
                return JsonResponse({'message': model.chat, 'last_msg': last_msg})

뷰에서 is_ajax()요청을 받는 과정에서 dbTrigger()실행과정에 ajax요청을 보내 Null값을 가지는 chat이 저장되는 문제가 발생했다. 꺼림직하긴한데 우선 dbTrigger()과 메세지전송시 실행되는 ajax를 구분해 처리하기 위해 전자는 {"dbtrigger":1}을 후자는 {"dbtrigger":0}을 데이터로 보내주었고 뷰에서 구분해 처리했다. dbtrigger가 0인 즉 메세지전송의 경우에만 model.save()가 발생하도록하였다. 또한 데이터가 과도하게 쌓이는것을 방지하기위해 메세지는 50개이상쌓이면 자동삭제되게 처리하였다.

//landing-chat 채팅화면렌더링함수
function chatRender() {
    $.ajax({
        type: "GET",
        url: "/chat/",
        data: {"message": $('#chat-box').val()},
        success: function(res) {
            var e = $(res).find('#chat-box').children();
            $('#chat-box').children().remove();
            $('#chat-box').html(e);
            $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
        }
    });
}
//landing-chat db trigger
function dbTrigger() {
    $.ajax({
         type: "POST",
         url: "/chat/",
         beforeSend: function(xhr) {
             xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
         },
         data: {"dbtrigger": 1},
         success: function(res){
            if(lastDate<res.last_msg) {
                chatRender()
                lastDate = res.last_msg
            }
            else {
                lastDate = res.last_msg
            }
         },
         error: function (res) {
             alert("dbTrigger 에러")
         }
    });
}
$(document).ready(function(){
	//  LANDING-CHAT
    //엔터감지
    $('#landing-chat-input').keydown(function(key){
        if(key.keyCode == 13){
            $('#landing-chat-send').click();
        }
    });
    //최초렌더링
    chatRender();

    //0.5초마다 db감시
    setInterval(dbTrigger, 500);
    $('#landing-chat-send').on("click", (e) => {
        message = $('#landing-input-area').val();
        $.ajax({
            type: "POST",
            url: '/chat/',
            data: {"message": message, "dbtrigger": 0},
            dataType: "json",
            beforeSend: function(xhr) {
                xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
            },
            success: function (res) {
                chatRender();
                setTimeout(function(){
                    $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
                },100)
                $('#landing-input-area').val('');
            },
            error: function (res) {
                alert("전송실패. 지속될 경우 에러제보페이지에 문의바랍니다.")
            }
        });
    });
});

 

dbTrigger()가 0.5초마다 계속 동작하기 때문에 리소스를 더 아낀다거나 하는 차이를 주지는 못한것 같지만 이전버전처럼 계속 화면을 렌더링하는 것이 아니라 메세지를 보낼때마다 화면이 렌더링되도록 동작하게 할 수 있었다. 템플릿에 유저 이미지를 포함했다보니 반복렌더링되는과정에서 이미지가 조금 깜빡거리는듯한 느낌이 있었는데 dbTrigger()를 구현함으로써 해결되었다.

 


에러테스트하다가 문제가 있어서 추가한다.

현재 로직은 마지막 메세지의 시간을 받아 새롭게 글이들어왔는지 탐지하고 그렇다면 chatRender()를 작동시켜 화면에 요소를 재구성해 화면을 띄워주는 방식이다. 하지만 글을 처음 작성할때 즉, 받아올 마지막 메세지가 존재하지 않는다면 dbTrigger() 의 ajax는 error로 진입한다. 이 문제를 해결하기 위해 뷰에서 db가 존재하지 않을 경우를 나눠줬고 해당 경우에 현재시간을 받아 last_msg에 담아 JsonResponse해주었다. 그리고 db비존재의 경우 checkFirstChat:1을 존재의 경우 0을 담아 보냈고 dbTrigger()에서 이를 받아 db가 없으나 시간갱신으로 인해 chatRender() 조건으로 빠지는 문제가 발생하는 경우를 차단했다. 코드 덩어리가 된것같아 불편하지만 우선 이제 에러는 없는 것 같다.

class ChatView(View):

    def get(self, request):
        message = LandingChat.objects.all().order_by('date')
        return render(request, 'chatapp/landing_chat.html', {'message': message})


    def post(self, request):
        if request.is_ajax():
            #데이터 아무것도 없으면 받아오는 결과값이 없어 dbTrigger error발생할것
            if LandingChat.objects.all().count()==0:
                now = datetime.now()
                dbtrigger = int(request.POST.get('dbtrigger', 0))
                if not (dbtrigger):
                    model = LandingChat()
                    model.chat = request.POST.get('message')
                    model.writer = request.user
                    model.save()

                return JsonResponse({'last_msg': now, 'checkFirstChat': 1})
            #챗데이터가 이미 존재하는경우
            else:
                dbtrigger = int(request.POST.get('dbtrigger', 0))
                if (dbtrigger):
                    model = LandingChat()
                    last_msg = LandingChat.objects.latest('date').date
                    return JsonResponse({'message': model.chat, 'last_msg': last_msg, 'checkFirstChat': 0})
                else:
                    model = LandingChat()
                    model.chat = request.POST.get('message')
                    model.writer = request.user
                    model.save()
                    if LandingChat.objects.all().count()>50:
                        LandingChat.objects.earliest('date').delete()
                    last_msg = LandingChat.objects.latest('date').date
                    return JsonResponse({'message': model.chat, 'last_msg': last_msg})

 

//landing-chat db trigger
function dbTrigger() {
    $.ajax({
         type: "POST",
         url: "/chat/",
         beforeSend: function(xhr) {
             xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
         },
         data: {"dbtrigger": 1},
         success: function(res){
            if((lastDate<res.last_msg)&&(checkFirstChat==0)) {
                chatRender()
                lastDate = res.last_msg
            }
            else {
                lastDate = res.last_msg
                checkFirstChat=res.checkFirstChat
            }
         },
         error: function (res) {
             alert("dbTrigger 에러. 네트워크를 확인하세요")
         }
    });
}
728x90
반응형