Django에서 SQLite를 사용하는 경우, 별도로 데이터베이스를 설치할 필요가 없습니다. SQLite는 Django의 기본 내장 데이터베이스 엔진 중 하나이며, 기본적으로 Django 프로젝트를 생성할 때 설정되어 있는 데이터베이스입니다.SQLite는 파일 기반의 경량 데이터베이스로, 데이터베이스 서버가 별도로 실행되지 않고 Django 애플리케이션 내부에서 데이터를 저장합니다. 따라서 SQLite를 사용할 때 별도로 데이터베이스를 설치할 필요가 없습니다.
간단한 투표 애플리케이션을 만들 때, Django의 ORM(Object-Relational Mapping)을 사용하여 모델을 정의하고 이를 통해 SQLite 데이터베이스에 데이터를 저장하고 검색할 수 있습니다.
이번 포스팅에서는 Django를 사용하여 간단한 투표 애플리케이션을 만들어 보겠습니다.
먼저, 데이터베이스에 저장될 question모델과 choice 모델을 만듭니다.
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=100)
pub_date = models.DateTimeField()
def __str__(self):
return self.question_text
# 이 메서드는 해당 객체를 문자열로 표현할 때 사용됩니다.
# Django Admin 등에서 해당 모델 객체를 출력할 때,
# question_text 필드의 값을 반환하여 보기 쉽게 표시합니다.
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=20)
votes = models.IntegerField(default=0)
# Question 모델:
# - question_text: 최대 100자의 질문 내용을 저장하는 필드
# - pub_date: 질문이 게시된 날짜와 시간을 저장하는 필드
# Choice 모델:
# - question: 해당 선택지가 어떤 질문에 속하는지를 나타내는 외래키
# - choice_text: 최대 20자의 선택지 내용을 저장하는 필드
# - votes: 선택지에 대한 투표 수를 나타내는 필드. 기본값은 0
# Django의 Admin 페이지 등에서 모델 객체를 출력할 때,
# Question은 question_text를 기준으로, Choice는 choice_text를 기준으로 출력됩니다.
model.py
질문항목을 작성해야하므로, 질문항목을 만들어주기 위해 관리자 파일도 만들어줍니다.
from django.contrib import admin
from myvote.models import Question, Choice
# 모델에 대한 Admin 설정 클래스 정의
class QuestionAdmin(admin.ModelAdmin):
# Question 모델의 Admin 설정
list_display = ('id', 'question_text', 'pub_date')
# Admin 패널에서 Question 객체를 보여줄 때, id, question_text, pub_date 표시
class ChoiceAdmin(admin.ModelAdmin):
# Choice 모델의 Admin 설정
list_display = ('id', 'question', 'choice_text', 'votes')
# Admin 패널에서 Choice 객체를 보여줄 때, id, question, choice_text, votes 표시
# 각 모델과 Admin 설정 클래스를 Admin 패널에 등록
admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice, ChoiceAdmin)
admin.py
그리고고 django의 localhost/admin으로 접속해서 원하는 질문을 작성하면 됩니다.
이제 url파일과 view, templates파일을 만들어보겠습니다.
from django.urls import path
from myvote import views
urlpatterns = [
path('', views.DispFunc, name='disp'),
path('<int:question_id>', views.DetailFunc, name='detail'),
path('<int:question_id>/vote/', views.VoteFunc, name='vote'),
path('<int:question_id>/results/', views.ResultFunc, name='results'),
]
urls.py
'int:question_id' 경로 (views.DetailFunc):
정수형으로 된 question_id가 주어졌을 때, views.DetailFunc 함수를 호출합니다.
URL 패턴의 경로에 질문 ID가 주어지며, 예를 들어 http://example.com/1/ 또는 http://example.com/2/와 같이 접속했을 때 이 함수가 실행됩니다.
'int:question_id/vote/' 경로 (views.VoteFunc):
정수형으로 된 question_id가 주어지고 그 뒤에 /vote/가 추가로 주어졌을 때, views.VoteFunc 함수를 호출합니다.
예를 들어, http://example.com/1/vote/ 또는 http://example.com/2/vote/와 같이 접속했을 때 이 함수가 실행됩니다.
'int:question_id/results/' 경로 (views.ResultFunc):
정수형으로 된 question_id가 주어지고 그 뒤에 /results/가 추가로 주어졌을 때, views.ResultFunc 함수를 호출합니다.
예를 들어, http://example.com/1/results/ 또는 http://example.com/2/results/와 같이 접속했을 때 이 함수가 실행됩니다.
다음은 메소드파일입니다.
from django.shortcuts import render, get_object_or_404
from myvote.models import Question, Choice
from django.http.response import HttpResponse, Http404, HttpResponseRedirect
from django.urls.base import reverse
# Create your views here.
def MainFunc(request):
return render(request, 'main.html')
def DispFunc(request):
# 질문 목록을 가져와서 display.html 템플릿으로 전달하는 함수
q_list = Question.objects.all().order_by('pub_date', 'id')
context = {'q_list': q_list} # context에 질문 목록을 담아 템플릿으로 전달
return render(request, 'display.html', context)
def DetailFunc(request, question_id):
# get_object_or_404를 사용하여 존재하는 질문인지 확인하고, 없으면 404 에러 반환
question = get_object_or_404(Question, pk=question_id)
return render(request, 'detail.html', {'question': question})
def VoteFunc(request, question_id):
# 사용자의 투표를 처리하는 함수
question = get_object_or_404(Question, pk=question_id)
try:
# POST 요청에서 선택한 항목에 대한 데이터를 받아옴
sel_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# 선택한 항목이 없는 경우 에러 메시지와 함께 detail.html을 렌더링
return render(request, 'detail.html', {'question': question, 'err_msg': '1개의 항목을 선택하세요'})
# 선택 항목의 투표 수를 1 증가시키고 저장
sel_choice.votes += 1
sel_choice.save()
# 결과 페이지로 이동하는 URL을 reverse를 사용하여 동적으로 생성하고 이동
return HttpResponseRedirect(reverse('results', args=(question.id,)))
def ResultFunc(request, question_id):
# 특정 질문에 대한 결과를 보여주는 함수
question = get_object_or_404(Question, pk=question_id)
return render(request, 'result.html', {'question': question})
views.py
get_object_or_404() 함수는 Django 단축 함수 중 하나로, 데이터베이스에서 특정 객체를 가져오거나 존재하지 않을 경우 404 오류를 발생시키는 기능을 합니다. 위 코드에서 get_object_or_404() 함수는 Question 모델에서 pk 값이 question_id와 동일한 객체를 가져오는 역할을 합니다. 즉, Question 모델에서 주어진 question_id와 일치하는 Primary Key 값을 가진 객체를 조회합니다.
이렇게 함으로써, 개발자는 일일이 조건문을 사용하여 객체를 검사하는 등의 번거로운 작업 없이, 한 줄의 코드로 객체를 가져올 수 있고, 객체가 존재하지 않는 경우 예외 처리를 할 수 있습니다.
+)
`return render(request, 'detail.html', {'question': question})`와
`return HttpResponseRedirect(reverse('results', args=(question.id,)))`는
Django 뷰(View)에서 각각 다른 응답을 클라이언트에게 반환하는 데 사용됩니다.
1. `return render(request, 'detail.html', {'question': question})`: 이 코드는 특정 템플릿 파일을 렌더링하여 사용자에게 보여주는 역할을 합니다. `detail.html` 템플릿을 렌더링하고, 해당 템플릿에 `question` 변수를 전달합니다. 이는 일반적으로 웹 페이지에 정보를 표시할 때 사용됩니다. 사용자는 이를 확인할 수 있습니다. 하지만, 이것은 페이지 리다이렉트를 수행하지 않습니다.
2. `return HttpResponseRedirect(reverse('results', args=(question.id,)))`: 이 코드는 클라이언트의 요청을 다른 URL로 리다이렉트하는 역할을 합니다. `reverse('results', args=(question.id,))`는 URL 패턴의 이름을 사용하여 URL을 생성하고, 해당 URL로 클라이언트의 브라우저를 리다이렉트합니다. 즉, 사용자가 요청한 페이지 대신에 다른 페이지로 이동시키는 역할을 합니다. 일반적으로 어떤 작업을 마치고나서 사용자를 다른 곳으로 보내고자 할 때 사용됩니다.
따라서, `render()`는 템플릿을 렌더링하여 현재 페이지에 정보를 표시하는 데 사용되고, `HttpResponseRedirect`는 사용자를 다른 페이지로 리다이렉트시키는 데 사용됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
투표할 질문 항목 선택<p/>
{% if q_list %}
<ul>
{% for q in q_list %}
{# url 요청 직접 기술방법1(정적, 하드코딩) #}
<li><a href="/go/{{q.id}}">{{q.question_text}}</a></li>
<br/>
{# url 요청 직접 기술방법2(동적, 소프트코딩) #}
<li><a href="{% url 'detail' q.id %}">{{q.question_text}}</a></li>
<hr/>
{% endfor %}
</ul>
{% else %}
<div>질문 항목이 없어요</div>
{% endif %}
</body>
</html>
display.html
직접 기술 방법 1 (정적, 하드코딩):
<a href="/go/{{ q.id }}">{{ q.question_text }}</a>와 같이 URL을 직접 작성합니다.
이 방법은 URL을 하드코딩하여 직접 지정하는 방법입니다. 즉, URL이 변경되면 해당 HTML 파일을 수정해야 합니다.
URL의 변경이 필요할 때마다 HTML 파일을 수정해야 하므로 유지보수가 어려울 수 있습니다.
직접 기술 방법 2 (동적, 소프트코딩):
<a href="{% url 'detail' q.id %}">{{ q.question_text }}</a>와 같이 {% url %} 템플릿 태그를 사용합니다.
이 방법은 Django의 내부 URL 패턴 이름을 사용하여 URL을 동적으로 생성합니다.
URL 패턴의 이름('detail')과 해당 뷰에서 사용하는 매개변수(q.id)를 템플릿에서 사용하여 URL을 생성합니다.
URL 패턴이 변경되어도 템플릿 파일을 수정할 필요 없이, URL 패턴 이름만 유지하여 동작하도록 유지보수가 편리합니다.
일반적으로 URL을 직접 지정하는 방법보다는 Django의 {% url %} 템플릿 태그를 사용하여 동적으로 URL을 생성하는 것이 좋은 방법입니다. 이렇게 하면 URL 패턴이 변경되어도 템플릿 파일을 수정할 필요가 없어 유지보수에 용이합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>{{question.question_text}}</h2>
<b style="color: red"> <!-- 투표 항목을 선택하지 않은 경우 에러 메시지 출력 -->
{% if err_msg %}
{{err_msg}}
{% endif %}
</b>
<form action="{% url 'vote' question.id %}" method="post">{% csrf_token %}
{% for cho in question.choice_set.all %} {# choice object를 반환 #}
<input type="radio" name="choice" id="cho{{forloop.counter}}" value="{{cho.id}}">
{# forloop.counter : 현재까지의 loop를 실행한 반복 횟수 : 1부터 시작 #}
<label for="cho{{forloop.counter}}">{{cho.choice_text}}</label>
{% endfor %}
<br/><br/>
<input type="submit" value="투표 확인">
</form>
</body>
</html>
detail.html
form action="{% url 'vote' question.id %}" method="post">{% csrf_token %}:
폼의 action 속성은 사용자가 폼을 제출할 때 이동하는 URL을 설정합니다. {% url 'vote' question.id %}는 'vote'라는 이름의 URL 패턴을 동적으로 생성하며, question.id는 해당 질문에 대한 고유한 식별자입니다. method="post"는 폼을 제출하는 HTTP 메서드를 설정합니다.
{% for cho in question.choice_set.all %}: 이 부분은 현재 질문(question)과 연결된 선택지(Choice)들을 가져오는데 사용됩니다. choice_set은 Question 모델과 Choice 모델 사이의 ForeignKey 관계를 통해 Question 객체에 연결된 모든 Choice 객체들을 가져오는 역할을 합니다.
<input type="radio" name="choice" id="cho{{forloop.counter}}" value="{{cho.id}}">:
각 선택지에 대해 라디오 버튼을 생성합니다. name="choice"는 이 라디오 버튼이 같은 그룹으로 묶이도록 하는 이름입니다. id="cho{{forloop.counter}}"는 각 라디오 버튼의 고유한 ID를 생성합니다. value="{{cho.id}}"는 선택지의 ID를 해당 라디오 버튼의 값으로 설정합니다.
<label for="cho{{forloop.counter}}">{{cho.choice_text}}</label>:
각 라디오 버튼 옆에 표시되는 라벨입니다. for 속성은 라디오 버튼의 ID를 가리킵니다. {{cho.choice_text}}는 선택지의 텍스트를 표시합니다. 이 폼은 Django 템플릿 언어(DTL)을 사용하여 특정 질문에 대한 선택지를 라디오 버튼으로 보여주고, 사용자가 선택한 항목을 서버로 제출할 수 있는 HTML 폼을 생성합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>{{question.question_text}}</h2>
결과 : <br>
<ul>
{% for cho in question.choice_set.all %}
<li>
{{cho.choice_text}} - {{cho.votes}}표
</li>
{% endfor %}
</ul>
<a href="{% url 'detail' question.id %}">같은 항목 다시 투표</a>
<br/>
<a href="/go">투표 초기 화면</a>
</body>
</html>
result.html
<h2>{{ question.question_text }}</h2>:
question 객체의 question_text 필드를 출력하여 질문의 제목을 보여줍니다.
{% for cho in question.choice_set.all %}:
해당 질문과 관련된 모든 Choice 객체를 가져와 순회합니다.
{{ cho.choice_text }} - {{ cho.votes }}표:
각 선택지의 텍스트와 해당 선택지에 대한 투표 수(votes)를 출력합니다.
<a href="{% url 'detail' question.id %}">같은 항목 다시 투표</a>:
현재 질문에 대한 투표를 다시 할 수 있는 링크를 제공합니다. question.id는 현재 질문의 ID를 나타냅니다.
<a href="/go">투표 초기 화면</a>:
투표 초기 화면으로 돌아가는 링크입니다. '/go' URL로 이동합니다.
참고 개념
`{% csrf_token %}`은 Django에서 Cross Site Request Forgery(CSRF) 공격으로부터 웹 애플리케이션을 보호하기 위한 보안 기능 중 하나입니다. CSRF 공격은 사용자가 의도하지 않은 요청을 악의적으로 보낼 때 발생할 수 있는 보안 취약점입니다. 예를 들어, 사용자가 이미 로그인한 상태에서 악의적인 웹 사이트를 방문하거나, 이메일 링크를 통해 들어온 페이지에서 악성 요청을 보내는 경우가 해당됩니다.
Django에서는 이러한 CSRF 공격으로부터 애플리케이션을 보호하기 위해 폼 제출 과정에서 추가적인 보안 검증을 수행합니다. `{% csrf_token %}` 템플릿 태그는 Django에서 제공하는 특별한 토큰을 생성하여 폼 안에 포함시킵니다. 이 토큰은 서버 측에서 요청을 처리할 때 검증되어, 해당 요청이 올바른 애플리케이션으로부터 온 것인지 확인합니다.
따라서, Django를 사용하는 경우 폼을 POST 방식으로 제출할 때마다 `{% csrf_token %}`을 사용하여 CSRF 공격을 방지하는 보안 검증 토큰을 포함시켜야 합니다.