TITEDIOS 편한 코딩

[Django] 폼(form)태그로 데이터 전송하기 - 투표 앱 만들기(Django tutorial) 본문

Django

[Django] 폼(form)태그로 데이터 전송하기 - 투표 앱 만들기(Django tutorial)

TitediosKW 2024. 11. 15. 19:00
반응형

목차

  1. 간단한 폼 쓰기
    1) form 태그 추가
    2) View(뷰) 동작 구현
    3) 결과를 출력하는 화면 구현
  2. 제너릭 뷰 사용
  3. 결론

form(폼) 태그는 사용자로부터 데이터를 입력받아 서버로 전송하는 웹 페이지의 영역을 정의할 때 사용됩니다. 주로 회원가입, 로그인, 댓글 작성 같은 입력 기능을 만들 때 쓰이죠. form(폼) 태그 안에는 입력 필드를 제공하는 다양한 태그들 (input, textarea, select 등)이 포함되어 다양한 정보를 전달할 수 있습니다. 이번 포스팅에서는 장고에서 form(폼) 태그를 이용하는 방법에 대해 실습해 보겠습니다.


1. 간단한 폼 쓰기

1) form 태그 추가

우선 form을 사용하기 위해 polls/templates/polls/detail.html 파일을 아래와 같이 수정해 보겠습니다.

<form action="{% url 'polls:vote' question.id %}" method="post">  
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

위 코드를 간단하게 설명하도록 하겠습니다.

  • (form 동작 설정) form의 action을 {% url 'polls:vote' question.id %}로 설정하고, method="post"로 설정합니다. 이 form을 제출하는 행위는 데이터 서버 측을 변화시키기 때문에 method="post" (method="get"과 반대로)를 사용하는 것은 매우 중요합니다. 서버 측의 데이터를 변경하는 form을 만들 때는 method="post"를 사용하시는 것이 좋습니다. 이 것은 Django에만 국한된 것이 아니라 일반적인 좋은 웹 개발 습관입니다.
  • (CSRF) POST 방식의 form을 만들고 있기 때문에(데이터를 수정하는 효과가 있을 수 있음), 교차 사이트 요청 위조(Cross Site Request Forgeries)에 대해 신경 써야 합니다. 하지만, 여러분은 너무 걱정하지 않아도 됩니다. 왜냐하면 Django는 CSRF를 보호하는 유용한 시스템이 있기 때문입니다. 간단히 말하자면, 내부 URL을 대상으로 하는 모든 POST 양식은 {% csrf_token %} 템플릿 태그를 사용하기만 하면 CSRF로부터 여러분의 웹을 보호할 수 있습니다.
  • (라디오 버튼 표시) 위의 템플릿은 각 Question에 대한 선택 항목을 라디오 버튼으로 표시합니다. 각 라디오 버튼의 value는 연관된 질문 선택 항목의 ID이고 각 라디오 버튼의 name은 "choice"입니다. 즉, 누군가가 라디오 버튼 중 하나를 선택하여 폼을 제출하면, POST 데이터 인 choice=#을 보낼 것입니다. 여기서 #은 선택한 항목의 ID입니다. 이것이 HTML 폼이 동작하는 방법입니다.
  • (반복 작업) forloop.counter 는 for 태그가 반복을 한 횟수를 나타냅니다.

2) View(뷰) 동작 구현

데이터를 전송할 화면을 만들었습니다. 그렇다면 접근이 가능하도록 URL 주소와 실제 동작을 구현하겠습니다. 사실 이미 URL 주소는 아래와 같이 있습니다.

polls/url.py 파일

path("<int:question_id>/vote/", views.vote, name="vote"),

 

그럼 이제 View만 구현하면 됩니다. polls/views.py파일을 수정하여 아래와 같이 구현하도록 하겠습니다.

from django.db.models import F
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question


# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(
            request,
            "polls/detail.html",
            {
                "question": question,
                "error_message": "You didn't select a choice.",
            },
        )
    else:
        selected_choice.votes = F("votes") + 1
        selected_choice.save()

        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
  • (request 객체) request.POST 는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체입니다. 이 경우, request.POST ['choice']는 선택된 설문의 ID를 문자열로 반환합니다. request.POST의 값은 항상 문자열들입니다.
  • Django는 같은 방법으로 GET 자료에 접근하기 위해 request.GET를 제공합니다 – 그러나 POST 요청을 통해서만 자료가 수정되게 하기 위해서, 명시적으로 코드에 request.POST를 사용하고 있습니다. 만약 POST 자료에 choice 가 없으면, request.POST ['choice']는 KeyError 가 일어납니다. 위의 코드는 KeyError를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시 보여줍니다.
  • (DB 작업) F("votes") 함수는 선택된 Choice의 내부에 votes라는 필드의 값을 1 증가시키는 함수입니다. DB에서 작업을 해야 할 경우 F 함수를 사용하여 작업하실 수 있습니다.
  • (결과 반환) 설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect를 반환하고, HttpResponseRedirect는 하나의 인수를 받습니다: 그 인수는 사용자가 재전송될 URL입니다. POST 데이터를 성공적으로 처리한 후에는 항상 HttpResponseRedirect를 반환해야 합니다. 이 역시 Django에만 국한된 것이 아니라 일반적으로 좋은 웹 개발 습관입니다.
  • (Redirection 주소) 우리는 이 예제에서 HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용하고 있습니다. 이 함수는 뷰 함수에서 URL의 하드코딩을 방지해 줍니다. 뷰의 이름과 URL패턴의 변수 부분을 조합해서 해당 뷰를 가리키도록 합니다. 여기서 reverse() 호출은 아래와 같은 문자열을 반환할 것입니다.
"/polls/3/results/"

 

구현을 하고 python manage.py runserver 명령을 수행합니다. 이후에 http://localhost:8000/polls//votes URL에 접근하시면 아래와 같이 결과화면을 보실 수 있습니다.

결과 화면

반응형

3) 결과를 출력하는 화면 구현

지금까지 했던 것들을 되짚어 봅시다. 우리 지금 Question에 대한 Choice들을 선택하고 투표한 결과를 저장하기 위해서 화면을 만들고 기능을 구현했습니다. 기억하시죠?

 

그럼 투표 후 결과를 한번 확인해 보는 것이 좋겠습니다. 먼저 결과화면을 만들어 보겠습니다.

polls/templates/polls/results.html 파일을 생성하고 아래와 같이 코드를 추가해 보겠습니다.

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

 

지금까지 잘 해오셨다면 크게 어렵지 않게 이해하실 것이라고 생각하고 설명은 생략하겠습니다.

 

이제 View(뷰)를 구현해 보겠습니다. polls/views.py 파일을 아래와 같이 수정하겠습니다.

from django.shortcuts import get_object_or_404, render

...

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

역시 크게 어렵지 않으므로 코드 설명은 생략합니다. 웹 페이지에서 투표를 한번 해보세요. 그러면 아래와 같이 결과 화면을 보실 수 있으십니다. 저는 1번과 3번 선택지에 한 번씩 투표를 하였습니다.

투표 후 결과 화면


2. 제너릭 뷰 사용

실습하느라고 고생하셨습니다. 그런데 한 가지 생각해 볼 점이 있는 거 같습니다. detail.html과 result.html이 표시되는 화면 즉, 상세화면과 결과화면의 구현을 살펴보겠습니다. 그러면 사실 굉장히 단순합니다.

...

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/detail.html", {"question": question})


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

...

 

그렇다면, 이런 정도는 Django라는 프레임워크가 알아서 해줄 수도 있지 않을까요? Django를 개발한 개발자들이 그 정도의 생각도 못하진 않을 테니까요. 이러한 단순한 작업은 제너릭 뷰(generic view)를 통해 해결할 수 있습니다. 제너릭 뷰를 이용하기 위해 polls/views.py 파일을 아래와 같이 수정해 보겠습니다. 아쉽게도 vote() 함수는 제너릭 뷰로 전환하기 어렵습니다 ㅠㅜ.

from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"


class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"


def vote(request, question_id):
    # same as above, no changes needed.
    ...
  • 각각의 generic view는 어떤 모델을 사용할지 알아야 하며, 이를 위해 model 속성을 사용하거나 get_queryset() 메서드를 정의하여 제공할 수 있습니다.
  • (generic.ListView) IndexView 클래스에서 사용한 ListView generic view는 기본적으로 (app name)/(model name)_list.html이라는 템플릿을 사용합니다. 여기서는 template_name을 사용하여 ListView가 기존의 "polls/index.html" 템플릿을 사용하도록 지정합니다.
  • (generic.DetailView) 마찬가지로 DetailView generic view는 (app name)/(model name)_detail.html이라는 템플릿을 사용합니다. 예를 들어, 이 경우에는 polls/question_detail.html이라는 템플릿이 사용됩니다. template_name 속성을 통해 자동 생성된 기본 템플릿 이름 대신 특정 템플릿 이름을 사용하도록 Django에 지정할 수 있습니다. ResultsView 뷰를 위해서도 template_name을 지정했는데, 이렇게 하면 결과 뷰와 세부 정보 뷰가 모두 DetailView이지만, 서로 다른 모습으로 렌더링 됩니다.
  • DetailView의 경우 Django 모델(Question)을 사용하고 있기 때문에 Django가 적절한 콘텍스트 변수 이름을 자동으로 결정하여 question 변수를 제공해 줍니다. 그러나 ListView의 경우 자동으로 생성되는 콘텍스트 변수는 question_list입니다. 이를 덮어쓰기 위해 context_object_name 속성을 사용하여 latest_question_list라는 변수를 사용하도록 지정했습니다. 대신, 템플릿을 수정하여 새로운 기본 컨텍스트 변수 이름을 맞출 수도 있지만, 원하는 변수를 사용하도록 Django에 지시하는 것이 훨씬 간편합니다.
  • generic views에 대한 자세한 내용은 (generic views 문서)[https://docs.djangoproject.com/en/5.1/topics/class-based-views/]에서 확인할 수 있습니다.

여러분이 저와 마찬가지로 runserver명령을 계속 실행 중이셨다면 콘솔에서 바로 에러가 발생하는 것을 확인하실 수 있습니다. 이것은 URL을 제너릭 뷰에 맞게 고쳐주지 않아서 발생하는 에러입니다. polls/urls.py를 수정해 준다면 해결됩니다.

from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

결론

Django에서 폼 태그와 제너릭 뷰를 사용하면 사용자 입력을 서버로 쉽게 전송하고, 단순한 화면 처리를 자동화할 수 있습니다. 폼 태그를 활용하여 사용자의 투표 데이터를 POST 방식으로 서버에 안전하게 전달하고, 이를 처리해 투표 결과를 저장합니다. 이때 Django의 CSRF 보호 기능을 사용하여 보안도 강화할 수 있으며, URL 패턴과 뷰 함수의 간단한 설정을 통해 데이터를 쉽게 처리합니다.

 

Django의 제너릭 뷰(generic view)를 사용하면 단순한 화면 렌더링을 쉽게 자동화할 수 있습니다. 예를 들어 DetailView와 ListView는 모델을 지정해주기만 하면 템플릿과 콘텍스트 변수를 자동으로 설정하고, context_object_name과 template_name을 통해 사용자 지정도 가능합니다. 제너릭 뷰로 코드 간결성을 높이고 유지보수를 쉽게 할 수 있어, Django 개발에서 매우 유용한 도구입니다.

 

조금 더 Django와 친해지는 시간이 되셨길 바라면서 도움이 되셨다면 공감 부탁드리겠습니다. 여러분의 공감이 정말 큰 힘이 됩니다.

 

감사합니다!

반응형