Django - Vue.js와 연동하기
Web/Django

Django - Vue.js와 연동하기

뉴비뉴 2019. 8. 21.

todo 앱을 생성해주고, 초기설정(superuser)을 해준 뒤 

 

1. 모델 만들기

# todo/models.py

from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=100)
    completed = models.BooleanField()

    def __str__(self):
        return self.title

python manage.py makemigrations

python manage.py migrate

 

2. 관리자 페이지 등록하기

# todo/admin.py


from django.contrib import admin

from .models import Todo


class TodoAdmin(admin.ModelAdmin):
    list_display = ['title','completed']
    
    
admin.site.register(Todo, TodoAdmin)

 

3.뷰 만들기

# todo/views.py


from django.shortcuts import render

from django.http import JsonResponse

from todo.models import Todo

# 저장 된 모든 할 일 데이터를 불러온 후에 하나씩 json 데이터로 가공할 수 있게 사전형 데이터로 저장
# 마지막으로 JsonResponse 형태로 데이터를 반환하면 Vue.js앱에서 데이터를 받아서 보여주게된다.
def todo_fetch(request): # 목록 불러오기
    todos = Todo.objects.all()
    todo_list = []
    for index, todo in enumerate(todos, start=1): # eumerate는 반복문 사용 시 몇 번째 반복문인지 돌려준다.
        todo_list.append({'id':index,'title':todo.title,'completed':todo.completed})

    return JsonResponse(todo_list, safe=False)

import json
from django.views.decorators.csrf import csrf_exempt
from .forms import TodoForm


@csrf_exempt
def todo_save(request): #할 일 목록 전체 데이털르 받아서 그대로 저장하는 역할을 한다.
    if request.body:
        data = json.loads(request.body)
        if 'todos' in data: #3 그런데 뷰를 호출 할 때 마다 데이터 확인 없이 데이터를 지우게되면 문제가 발생함으로 확인
            todos = data['todos']
            Todo.objects.all().delete() #2 전체 데이터를 지우고 다시 입력하는 방식 사용
            for todo in todos:
                print('todo', todo)
                form = TodoForm(todo) # 데이터를 저장
                if form.is_valid():
                    form.save() # DB에 저장
    return JsonResponse({})

 

4. 폼 생성

# todo/forms.py


from django import forms

from .models import Todo


class TodoForm(forms.ModelForm):
    class Meta:
        model = Todo
        fields = ['title','completed']

 

5. URL 연결하기

# config/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('todo.urls'))
]


# todo/urls.py

from django.urls import path

from .views import *

urlpatterns = [
    path('fetch/', todo_fetch, name='fetch'),
    path('save/', todo_save, name='save'),
]

http://127.0.0.1:8000/fetch/

fetch/ 로 접근하면 미리 관리자 페이지를 통해 만들어둔 할 일 목록이 나타난다.

save/ 로 접근하면 비어있는 사전형 데이터 하나만 나온다.

todo_save 뷰는 POST 메서드로 데이터를 전송해야만 제대로 기능을 하기 때문입니다.

 

이런 비어있는 뷰를 보여주기 싫고 POST 멧더르르 사용했을 때만 뷰가 동작하도록 하고 싶다면 데코레이터를 이용해서 제약 조건을 만들 수 있습니다.

# todo/urls.py



< 이전 코드 그대로 > 

from django.views.decorators.http import require_POST

from .forms import TodoForm
@csrf_exempt
@require_POST # POST 메서드로 접근했을 때만 동작
def todo_save(request):
    if request.body:
        data = json.loads(request.body)
        if 'todos' in data:
            todos = data['todos']
            Todo.objects.all().delete()
            for todo in todos:
                form = TodoForm(todo)
                if form.is_valid():
                    form.save()
                    
    return JsonResponse({})

 

6. Vue.js 템플릿 적용

<!-- templates/base.html -->

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Django Todo with Vue.js{% endblock %}</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <link rel="stylesheet" type="text/css" href="https://unpkg.com/todomvc-app-css@2.0.6/index.css">
</head>
<body>
<section class="todoapp">
    <header class="header">
        <h1>todos</h1>
        <input type="text" class="new-todo" 
               autofocus autocomplete="off"
        placeholder="What needs to be done?"
        v-model="newTodo"
        @keyup.enter=addTodo">
    </header>
    {% block content %}
    {% endblock %}
</section>
<footer class="info">
    <p>Double-click to edit a todo</p>
    <p>Written by <a href="http://evanyou.me">Evan You</a></p>
    <p>Edited by <a href="https://github.com/2044smile">2044smile</a>Ver. Django</p>
    <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>

{% block extra_script_footer %}
{% endblock %}
</body>
</html>
<!-- todo/templates/todo/list.html -->

{% extends 'base.html' %}

{% block content %}
    <section class="main" v-show="todos.length" v-cloak>
        <input type="checkbox" class="toggle-all" v-model="allDone">
        <ul class="todo-list">
            <li v-for="todo in filteredTodos"
                class="todo"
                :key="todo.id"
            :class="{ completed: todo.completed, editing: todo == editedTodo}">
                <div class="view">
                    <input type="checkbox" class="toggle" v-model="todo.completed">
                    <label @dblclick="editTodo(todo)">[[ todo.title ]]</label>
                    <button class="destroy" @click="removeTodo(todo)"></button>
                </div>
                <input type="text" class="edit"
                v-model="todo.title"
                v-todo-focus="todo == editedTodo"
                @blur="doneEdit(todo)"
                @keyup.enter="doneEdit(todo)"
                @keyup.esc="cancelEdit(todo)">
            </li>
        </ul>
    </section>
    <footer class="footer" v-show="todos.length" v-cloak>
        <span class="todo-count">
            <strong>[[ remaining ]]</strong> [[ remaining | pluralize ]] left
        </span>
    <ul class="filters">
        <li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li>
        <li><a href="#/active" :class="{ selected : visibility == 'active' }">Active</a></li>
        <li><a href="#/completed" :class="{ selected: visibility == 'completed'">Completed</a></li>
    </ul>
    <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">Clear completed</button>
    </footer>
{% endblock %}

{% block extra_script_footer %}
    {% load static %}
    <script src="{% static 'js/scripts.js' %}"></script>
{% endblock %}

 

7. 정적 파일 적용

// todo/static/js/scripts.js


// Full spec-compliant TodoMVC with localStorage persistence
// and hash-based routing in ~120 effective lines of JavaScript.

// localStorage persistence
Vue.prototype.$http = axios;
// axios라는 HTTp 클라이언트 라이브러리 입니다. Promise 베이스이기 때문에 사용하기 까다롭지만
// Vue.js의 HTTP 클라이언트로 가장 많이 사용됩니다. axios를 Vue에서 기본으로 사용하도록 prototype에 추가

var todoStorage = {
    // 가장 많은 변경이 있는 부분입니다. 원래 코드는 데이터를 웹 스토리지에 저장하는데 이를 장고를 통해
    // 데이터베이스에 저장하고 꺼내와야 하기 때문에 axios를 사용해서 데이터를 저장하고 Vue.js의 기본 통신
    // 메서드를 사용해 데이터를 얻어오도록 코드를 변경햇습니다. Promise 패턴을 사용하는 fetch부분은
    // 자바스크립트가 익숙하지 않으신 분들에게는 어려울 수 있습니다.
  fetch: function (app) {
    app.fetch('fetch/').then((response) => {
        return response.json();
    })
    .then((todos) => {
        todos.forEach(function (todo, index) {
          todo.id = index
        });
        this.uid = todos.length
        app.app.todos = todos;

    });
    return []
  },
  save: function (todos) {
    app.$http.post('save/',{todos:todos});
  }
}

// visibility filters
var filters = {
  all: function (todos) {
    return todos
  },
  active: function (todos) {
    return todos.filter(function (todo) {
      return !todo.completed
    })
  },
  completed: function (todos) {
    return todos.filter(function (todo) {
      return todo.completed
    })
  }
}

// app Vue instance
var app = new Vue({
    // Vus.js 앱을 작성합니다.
  // app initial state
  delimiters: ['[[', ']]'], // 장고 템플릿 엔진과 충돌을 막기 위해 데이터를 적용하는 부분을 [[ ]] 로 변경
  data: {
    todos: todoStorage.fetch(this),
    newTodo: '',
    editedTodo: null,
    visibility: 'all'
  },

  // watch todos change for localStorage persistence
  watch: {
    todos: {
      handler: function (todos) {
        todoStorage.save(todos)
      },
      deep: true
    }
  },

  // computed properties
  // http://vuejs.org/guide/computed.html
  computed: {
    filteredTodos: function () {
      return filters[this.visibility](this.todos)
    },
    remaining: function () {
      return filters.active(this.todos).length
    },
    allDone: {
      get: function () {
        return this.remaining === 0
      },
      set: function (value) {
        this.todos.forEach(function (todo) {
          todo.completed = value
        })
      }
    }
  },

  filters: {
    pluralize: function (n) {
      return n === 1 ? 'item' : 'items'
    }
  },

  // methods that implement data logic.
  // note there's no DOM manipulation here at all.
  methods: {
    addTodo: function () {
      var value = this.newTodo && this.newTodo.trim()
      if (!value) {
        return
      }
      this.todos.push({
        id: todoStorage.uid++,
        title: value,
        completed: false
      })
      this.newTodo = ''
    },

    removeTodo: function (todo) {
      this.todos.splice(this.todos.indexOf(todo), 1)
    },

    editTodo: function (todo) {
      this.beforeEditCache = todo.title
      this.editedTodo = todo
    },

    doneEdit: function (todo) {
      if (!this.editedTodo) {
        return
      }
      this.editedTodo = null
      todo.title = todo.title.trim()
      if (!todo.title) {
        this.removeTodo(todo)
      }
    },

    cancelEdit: function (todo) {
      this.editedTodo = null
      todo.title = this.beforeEditCache
    },

    removeCompleted: function () {
      this.todos = filters.active(this.todos)
    }
  },

  // a custom directive to wait for the DOM to be updated
  // before focusing on the input field.
  // http://vuejs.org/guide/custom-directive.html
  directives: {
    'todo-focus': function (el, binding) {
      if (binding.value) {
        el.focus()
      }
    }
  }
})

// handle routing
function onHashChange () {
  var visibility = window.location.hash.replace(/#\/?/, '')
  if (filters[visibility]) {
    app.visibility = visibility
  } else {
    window.location.hash = ''
    app.visibility = 'all'
  }
}

window.addEventListener('hashchange', onHashChange)
onHashChange()

// mount
app.$mount('.todoapp')

 

8. 동작하는 뷰 만들기

# todo/views.py

from django.shortcuts import render

def index(request):
    return render(request, 'todo/list.html')

index 뷰는 list.html만 렌더링해서 보여주는 역할을 합니다. 실제 화면은 프론트 코드가 모두 로드된 후에 Vue.js가 화면을 렌더링 합니다. 그리고 데이터를 주고 받기 위해 todo_fetch와 todo_save 뷰를 호출하면서 애플리케이션이 동작하는 형태 입니다.

 

index 뷰도 URL을 연결해줍시다.

# todo/urls.py

urlpatterns = [
    path('', index, name='index'),
    ]

댓글

💲 추천 글