[모각코][210807] Django 클래스형뷰 mixin

Jinhyung Rhee·2021년 8월 7일
0
post-thumbnail

본 포스트는 Inflearn 'Vue.js - Django 연동 웹 프로그래밍' 강의 일부를 요약한 내용입니다

개발 방향

  1. todo-html
  • html 파일에 vue.js 코드를 작성하는 방식
  • todo 데이터가 사라지는 단점이 있어서 불안전한 웹 프로그램임
    - 웹서버나 데이터베이스 관련 코드 없이는 웹 프로그램이 될 수 없음
  1. todo-vue-only
  • 장고에 1번의 html 파일을 그대로 수용하는 방식
  • todo 데이터가 사라지는 단점이 있어서 불안전한 웹 프로그램임
    - vue와 django를 연동하는 프로그램을 개발할 때 vue코드는 장고의 html 파일 안에 들어가야 함
    - 브라우저에서 요청이 왔을 때 장고는 vue코드가 들어있는 html파일을 브라우저에 내려주면 브라우저에서 vue코드가 동작하는 것!
    - 장점 : todo item을 저장하거나 삭제할 때 페이지 이동이 발생하지 않음! (Vue.js에서 제공하는 SPA - Single Page Application 기능)
  1. todo-django-only
  • Vue.js 코드 없이 장고 코드 만으로 todo앱 개발
  • 완전한 웹 프로그램
  • 단점 : todo item을 저장하거나 삭제할 때 페이지 이동 발생(3개의 html)
    - 이를 해결할 수 있는 방법 : django-mixin
  1. todo-django-mixin
  • 장고 클래스형 뷰믹스인 기능을 활용하는 방식
  • 페이지 이동을 축소함!
    - 페이지 이동 없이 동일한 화면에서 글 추가
    - 팝업창을 이용해 동일한 화면에서 글 삭제
  • django로만 todoApp을 개발하되 페이지 이동을 최소화하는 방식

클래스형뷰 Mixin 사용

  • 이전 코딩 순서 : s - m - u - v - t
  • 이번 코딩 순서 : u - t - v (s, m은 변경사항 없음)
    - template 먼저 코딩을 하면서 generic view에 mixin을 어떻게 할 것인지 구상한 뒤 view 코딩
  • URL 설계
URL패턴뷰 이름템플릿 파일명
todo/mixin/TodoMOMCV(MultipleObjectMixin, CreateView)todo_form_list.html
todo/99/delete2TodoMixinDelV(DeleteView)없음(팝업창)
  • mixinView
    - todo_form_list.html에서는 한 화면에 form과 list가 같이 보여지게 됨
    - View입장에서 form은 CreateView가 담당하고 list는 ListView가 담당함
    - 이 두 개의 generic view를 하나로 조합하는 mixin기능 필요!

    주의 : view에서 template으로 넘겨줘야 할 context 변수가 어떤 것인지 체크해야 함!
    - 예제) form에서는 context변수 사용X, list에서는 object_list변수 사용

  • python에서는 다중 상속 시 상속 순서도 중요함!
    - CreateView먼저? or ListView먼저?
    - Django generic view document 정리 사이트(https://ccbv.co.uk/)
  • Django의 클래스형 뷰는 공동적으로 사용되는 단위 기능들을 mixin 클래스로 만든 뒤 이런 mixin 클래스들을 조합해서 generic view를 만드는 방식으로 구성되어 있음!

    (출처 : https://ccbv.co.uk/)
    • CreateView가 ListView보다 복잡한 이유는 form처리 기능(ProcessFormView)이 들어가 있기 때문!
    • form 처리를 하기 위해선 GET요청과 POST요청 모두 처리가 되어야 form처리가 완료가 됨!
    • CreateView의 get메서드와 post메서드 모두 ProcessFormView를 상속받고 있음!
      (출처 : https://ccbv.co.uk/)
    • 반면 ListView의 경우 get메서드만 존재하고 get메서드는 ProcessFormView를 상속받고 있지 않음!
      (출처 : https://ccbv.co.uk/)

다중 상속을 받을 때는 좀 더 복잡한 CreateView를 메인으로 두고 ListView기능은 mixin으로 처리하는 것이 에러 가능성을 줄이는 방법임!!

  • 코딩 예시 : (mixin, main)
    - mixin 먼저 쓰고 main은 나중에 씀!
    - 'mixin'은 메인 재료에 추가되는 첨가물을 의미함
  • ListView의 mixin 클래스

  • 우리가 필요로 하는 기능은 object_list라는 context변수를 template에 넘겨주는 기능임
    - getContextData() 기능
    - 이 기능은 multipleObjectMixin클래스에 들어있음
    (출처 : https://ccbv.co.uk/)
    • self.object_list를 미리 준비해두고 getContextData()메서드만 호출하면 object_list라는 context변수가 template으로 넘어가게 됨!

정리

  • 어떤 클래스들을 상속받아야 하는지 알아야 한다
    - MultipleObjectMixin을 먼저 상속받고, 메인 클래스로 CreateView를 상속받아야 함
    - (MultiplieObjectMixin, CreateView)
    • 이렇게 상속받으면 대부분의 기능은 CreateView에서 처리
    • CreateView에 없는 getContextData()기능(=context변수 object_list를 template에 넘겨주는 기능)은 MultipleObjectMixin을 첨가함으로써 가능해짐!
      • (주의) self.object_list 미리 준비해둬야 함 -> 주로 get메서드에서 구현됨

코드 예제

  1. url (path추가)
urlpatterns = [
    # 중략..
    
    # Multiple Object Mixin Create View
    path('mixin/', views.TodoMOMCV.as_view(), name="mixin"),
]
  1. template (todo_form_list.html 추가)
<!-- 중략... body 부분만 -->

{% block content %}
<div id = 'app'>
    <h1>My Todo App !</h1>
    <strong>서로 할 일이나 의견을 공유해 봅시다.</strong>
    <br>
    <!-- form을 보여주는 부분->
    <!-- form태그에는 action과 method속성이 필요함 -->
    <form class="inputBox" action="" method="POST"> {% csrf_token %} <!--csrf공격 방지 위한 토큰-->
        <input class="name" type="text" placeholder="name ..." name="name">
        <input class="item" type="text" placeholder="type anyting welcomed ..."
            name="todo"> <!-- 엔터 눌러도 add기능 되도록 구현 -->
        <!-- v-model대신 name속성으로 변경 = 변수명은 todoTable의 변수명과 동일 해야함(주의!) -->
        <button type="submit" class="btn btn-info btn-sm">ADD</button>
    </form>

    <!-- list를 보여주는 부분-->
    <ul class="todoList">
        <!-- v-for는 장고 템플릿 문법으로 대체 -->
        {% for todo in object_list %} <!-- listView에서는 object_list라는 context 변수를 넘겨줌 -->
        <li>
            <!-- vue.js {mustache문법} -> { { 장고 템플릿 문법 } } 으로 변경 -->
            <span>{{ todo.name }} :: {{ todo.todo }}</span> <!-- column명은 todo임!-->
            <!-- 삭제버튼 클릭했을 때의 동작 <a>태그로 넣어줌 : delete url요청 & 파라미터로 todoID 넣음 -->
            <span class="removeBtn"><a href="{% url 'todo:delete' todo.id %}">&#x00D7</a></span>
        </li>
        {% endfor %}
    </ul>

</div>
{% endblock %}]
  1. view (TodoMOMCV 클래스 추가)
class TodoMOMCV(MultipleObjectMixin, CreateView):
    # CreateView form처리 위한 것(model속성, fields속성, templatename속성 필요)
    model = Todo
    fields = '__all__'
    template_name = 'todo/todo_form_list.html'
    # redirect 위한 코드 (새로 만든 'mixin' url로 redirect)
    success_url = reverse_lazy('todo:mixin')

    # object_list context변수 만들기 위해 self.object_list 변수 미리 준비 -> get, post 메소드에서
    # CreateView의 get메서드 overriding
    def get(self, request, *args, **kwargs):
        # DB에서 레코드들을 꺼내오는 메서드 : get_queryset()
        # get_queryset()메서드는 MultipleObjectMixin과 CreateView 둘 다 가지고 있기 때문에 먼저 오는 클래스에서 상속받음!
        self.object_list = self.get_queryset()

        # 상위 클래스의 메서드 오버라이딩 시 인자는 동일하게 써줌 (특별한 경우에만 변경)
        return super().get(request, *args, **kwargs)
        # 상위 클래스의 get메서드 그대로 호출
        # => CreateView의 getContextData()메서드 호출
        # => getContextData()메서드에 의해서 obejct_list라는 context변수가 template에 넘어가게 됨!

    # CreateView의 post메서드 overriding
    # get메서드와 동일한 방식으로 overriding
    def post(self, request, *args, **kwargs):
        self.obejct_list = self.get_queryset()
        return super().post(request, *args, **kwargs)
  1. 'base.html'에 메뉴 추가
<body>

<!-- 중략 -->
  
  <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item mx-1 btn btn-primary">
          <a class="nav-link text-white" href="{% url 'home' %}">Home</a></li>
        <li class="nav-item mx-1 btn btn-primary">
          <a class="nav-link text-white" href="{% url 'todo:vonly' %}">VueOnly</a></li>
        <li class="nav-item mx-1 btn btn-primary">
          <a class="nav-link text-white" href="{% url 'todo:create' %}">DjangoOnly</a></li>
        <!-- DjangoMixin 메뉴 추가 -->
        <li class="nav-item mx-1 btn btn-primary">
          <a class="nav-link text-white" href="{% url 'todo:mixin' %}">DjangoMixin</a></li>

<!-- 중략 -->

이후 문제점

  • add는 화면 이동 없이 정상적으로 동일 화면에서 처리됨!
  • 문제는 삭제한 후에 redirect가 'todo/mixin'으로 다시 가는 것이 아니라 'todo/list'로 감
<!-- todo_form_list.html body 일부분 -->
    <ul class="todoList">
        <!-- v-for는 장고 템플릿 문법으로 대체 -->
        {% for todo in object_list %} <!-- listView에서는 object_list라는 context 변수를 넘겨줌 -->
        <li>
            <!-- vue.js {mustache문법} -> { { 장고 템플릿 문법 } } 으로 변경 -->
            <span>{{ todo.name }} :: {{ todo.todo }}</span> <!-- column명은 todo임!-->
            <!-- 삭제버튼 클릭했을 때의 동작 <a>태그로 넣어줌 : delete url요청 & 파라미터로 todoID 넣음 -->
            <span class="removeBtn"><a href="{% url 'todo:delete2' todo.id %}">&#x00D7</a></span>
        </li>
        {% endfor %}
    </ul>
  • 삭제 버튼을 눌렀을 때 이동하는 url을 'delete2'로 적용하고 'delete2' url을 새로 정의함
urlpatterns = [
    # 중략
    
    # delete2 url 추가
    path('<int:pk>/delete2/', views.TodoDelV2.as_view(), name='delete2'),
]
  • TodoDelV2 클래스 view에서 생성함
class TodoDelV2(DeleteView):
    model = Todo
    # template은 기존 template 사용
    template_name = 'todo/todo_confirm_delete.html'
    # 삭제 성공 후 redirect : mixin으로 변경
    success_url = reverse_lazy('todo:mixin')

정리
1. mixin페이지서에는 form과 list모두 보여줌
2. list부분에서 x표시를 눌러서 삭제 페이지(delete2)로 이동
3. 삭제 페이지(delete2)에서 삭제를 완료하면 list페이지(이전 Django-Only에서 사용하던 페이지)가 아니라 mixin페이지로 돌아오도록 구현

Advanced : 삭제 페이지 대신 팝업창으로 삭제하기

  • 기존 DeleteView는 get요청post요청 두 개 다 처리를 해야 삭제 완료가 되었음
    - x표시를 눌렀을 때 보여주는 화면(/todo/id/delete2/) : get요청으로 메시지와 confirm버튼 보여줌
    - confirm버튼을 눌렀을 때 post요청을 보내고 그 때 DB에서 레코드 하나를 삭제함

  • 수정할 부분 : get요청에 대한 처리를 생략하고 post요청처리만 하도록 설계
    -> deleteView에도 get과 post 메서드가 있음
    -> get은 단순히 form을 보여주는 로직임
    (출처 : https://ccbv.co.uk/)
    -> post는 delete메서드를 호출하고 delete에서 실제 삭제가 이루어짐

  • 팝업창 없이 그냥 바로 삭제

# views.py
class TodoDelV2(DeleteView):
    model = Todo
    # form을 보여주는 과정이 생략되었으므로 template_name은 불필요
    # template_name = 'todo/todo_confirm_delete.html'
    success_url = reverse_lazy('todo:mixin') # 삭제 성공 후 redirect : mixin으로 변경

    # 기존 로직에서 get요청이 왔을 때 페이지 이동 없이 바로 delete처리를 하도록 overriding
    def get(self, request, *args, **kwargs):
        return self.delete(request, *args, **kwargs)
  • 팝업창 띄운 뒤 confirm 후 삭제
    - bootstrap Modal Components 사용
    - Varying modal content (모달 창으로 데이터 넘겨주는 기능)
    • 모달 창에서 데이터를 추출하기 위해 JavaScript코드 필요
{% block extra-style %}
<style>
/* 중략 */
/* x표 위에 마우스 위치했을 때 색 변경 */
  .removeBtn:hover {
  	color: red;
  }
  .modal-footer a {
  	color: white;
  }
</style>
{% endblock %}
<!-- 중략 -->
<!-- 삭제버튼 클릭했을 때의 팝업창이 뜨도록 토글형식으로 설정하고 타겟을 id로 지정해줌-->
<!-- 정보를 넘겨주기 위해 data-id/name/todo 지정-->
<span class="removeBtn" data-toggle="modal" data-target="#myModal"
                  data-id="{{ todo.id }}" data-name="{{ todo.name }}" data-todo="{{ todo.todo }}">&#x00D7</span>
        </li>
        {% endfor %}
    </ul>
    <!-- Modal -->
    <div class="modal" id="myModal" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">Are you sure to delete ?</h5>
          </div>
          <div class="modal-body">
              body-text
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
            <button type="button" class="btn btn-danger btn-sm">
                <a href="">Delete</a></button>
          </div>
        </div>
      </div>
    </div>

</div>
{% endblock %}

<!-- 데이터를 전달받기 위한 JavaScript -->
{% block extra-script %}
<script>

$('#myModal').on('show.bs.modal', function (event) {
    var button = $(event.relatedTarget)
    var id = button.data('id')
    var name = button.data('name')
    var todo = button.data('todo')

    var modal = $(this)
    // name과 todo는 modal-body에 사용 -> body-text에 채워지는 내용
    modal.find('.modal-body').text(name + '::: '+ todo)
    // id는 url 만들때 사용 -> href의 값으로 채워지는 부분
    modal.find('.modal-footer a').attr('href', '/todo' + id + '/delete2/')

})
</script>
{% endblock %}

정리

  • DjangoMixin을 사용하면 UX(User Experience)가 더 좋아짐
    - DjangoOnly는 추가, 삭제 모두 페이지 이동이 발생함
    - DjangoMixin을 사용하면 페이지 이동이 발생하지 않고 추가, 삭제가 됨
    - 페이지 이동이 없다는 점에서 기능적으로 VueOnly와 거의 유사함
  • VueOnly와 DjangoMixin의 랜더링 차이점
    - VueOnly로 추가, 삭제시 처음에만 페이지 로딩이 발생하고 이후 추가적으로 발생하지 않음 (실제로 페이지 이동 발생x) ➡ Client rendering
    - DjangoMixin으로 추가, 삭제시 매번 페이지 로딩이 발생 (실제로 페이지 이동 발생O) ➡ Server rendering
    - 개발자도구-Network 상에서 확인 가능
profile
기록하는 습관

0개의 댓글