Django 사용해보기 - 2

Sawol·2021년 5월 13일
0

django

목록 보기
2/5
post-thumbnail

❗️ django tutorial를 보고 실습 내용을 정리한 글입니다.

잘못된 부분이 있다면 알려주세요 🙏

뷰 생성하기

뷰란 장고에서는 어플리케이션이 제공하는 특정 기능과 템플릿을 제공하는 웹페이지를 의미한다. 예를 들면 질문 페이지, 투표 페이지 등이 있다. 각 뷰는 python 함수 또는 클래스 기반이면 메서드로 표현된다. 장고는 요청된 url의 도메인 이름 뒷 부분을 검사하여 뷰를 선택한다.
생성할 뷰는 다음과 같다.

  • 최근 질문들을 표시할 페이지
  • 질문 내용, 투효를 할 수 있는 서식을 표시하는 페이지
  • 특정 질문에 대한 결과를 표시할 페이지
  • 특정 질문에 대해 특정 선택을 할 수 있는 투표 기능이 제공되는 페이지
# polls/views.py

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

urlpatterns 에 값을 추가하여 url로 뷰를 연결한다.

# polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

urlpolls/뒤에 int값이 붙으면 이를 question_id로 인식하고 detail 함수를 호출한다. 만약, urlquestion_id뒤에 results가 있으면 results 함수를 호출한다. vote 또한 마찬가지다.

한 예로 사용자가 polls/5/를 요청했다고 하면 장고는 mysite.urls 파이썬 모듈을 호출한다.

# mysite/urls.py
urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

urlpatterns를 보면 url 끝에 polls가 있으면 polls.urls 모듈을 호출하도록 저번 글에서 설정을 했었다. 그래서 남은 텍스트인 /5를 찾기위해 polls.urls 모듈로 이동하면 거기에 있는 '<int:question_id>/'와 텍스트가 일치하게되어 결과적으로 detail 뷰 함수가 호출된다.

detail(request=<HttpRequest object>, question_id=5)

이 예에서는 detail함수는 이런 파라미터를 가지게 된다.

뷰에 기능 추가하기

장고에서 뷰는 데이터베이스의 레코드 읽기, (장고 또는 파이썬에서 제공하는) 템플릿 시스템 사용, PDF 생성, XML 출력, 실시간으로 ZIP 파일 만들기 등 파이썬의 모든 라이브러리를 사용할 수 있다.
여러가지 기능을 뷰에 추가할 수 있으나, 각 뷰들은 두 가지 기능만큼은 꼭 있어야한다. 옳바른 요청이여서 HttpResponse 객체를 반환하거나, 404 not found 같은 예외를 발생하게 해야한다.

# polls/views.py

from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

이렇게 index를 수정하면 index뷰를 호출했을 때, 데이터베이스에 저장된 최소 5개의 질문이 작성일 순으로 출력된다. 이렇게 파이썬 코드로 하드코딩이 되어있어도 정상적으로 작동하지만, 만약 보여지는 방식만 살짝 바꾸는 경우라도 코드 자체를 수정해야한다. 이러한 문제점 때문에 장고는 뷰에서 사용할 수 있는 템플릿을 따로 만들어 파이썬 코드와 디자인을 분리한다.

템플릿 생성하기

템플릿은 앱 디렉토리에 templates라는 디렉토리에 저장하여야 한다. 그러면 장고가 스스로 여기서 템플릿을 찾는다. 이때 templates 바로 밑에 템플릿을 저장하는 것보다 templates/<앱이름>으로 디렉터리를 하나 더 만든 후 템플릿을 저장하는 것이 좋다.
이유는 이름이 첫번째로 일치하는 템플릿을 선택하도록 되어있어, 다른 앱의 템플릿을 선택할 수 있다. 그렇기때문에 새 디렉터리를 만들어 이를 구분한다.

# polls/templates/polls/index.html

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

템플릿 코드는 위와 같다. 우리가 흔히 아는 html 태그 외에도 장고만의 특징인 탬플릿 태그가 있는 것을 볼 수 있다. 브라우저는 html만 해석할 수 있기 때문에 탬플릿 태그를 이용하면 파이썬 코드를 html로 바꿔준다. 위 html 파일은 튜토리얼이기 때문에 짧게 만들었지만, 실제로는 완전한 html 문서를 사용해야한다.

# polls/views.py

from django.http import HttpResponse
from django.template import loader

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

하드 코딩된 index뷰를 다시 위처럼 수정하여 index.html를 연결한다. index.html 템플릿을 불러온 후, contexthtml에 전달한다. context'템플릿에서 쓰인 변수명' : '파이썬 객체' 형태의 딕셔너리 값이다.

404 에러 일으키기

# polls/views.py
from django.http import Http404
from django.http import HttpResponse
from django.template import loader

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
        template = loader.get_template('polls/detail.html')
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return HttpResponse(template.render({'question': question}, request))

detail 뷰가 요청된 Questionid가 없을 경우, 즉 detail 뷰에 질문이 없을 경우 404 not found를 발생시킨다. detail 뷰의 템플릿인 polls/detail.html을 만들어야 한다.

# polls/templates/polls/detail.html

{{ question }}

템플릿 살펴보기

# polls/templates/polls/detail.html

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

장고에서 템플릿은 점-탐색(dot-lookup) 문법을 사용한다. 위 예제의 question.question_text에서 가장 먼저 question 객체에 대해 딕셔너리형태로 탐색을 한다. 실패를 하면 속성 값으로 탐색을 하고 이 예제는 속성값이기 때문에 탐색이 종료되지만 만약 실패한다면 리스트의 인덱스 탐색을 시도한다.
question.choice_set.allquestion 모델을 외래키로하는 모든 choice 모델의 값을 불러오는 명령어이다. 즉 질문에 대한 선택지 값을 모두 들고온다.

하드코딩 피하기

# polls/index.html

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

이 코드는 polls/부분이 하드코딩 되어있다. 이럴경우 url 하나를 바꾸는데 html코드와 파이썬 코드 모두 바꿔야하는 어려운 일이 된다. polls.urls 모듈의 path()함수에서 인수의 이름을 정의했으므로 {% url %} 태그를 사용해서 하드코딩을 없애야한다.

# polls/index.html

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

만약 detail 뷰의 urlpolls/specifics/12로 바꾸고 싶다면, 템플릿을 바꾸는 것이 아니라 polls/urls.py 하나만 바꾸면 된다.

# polls/urls.py

...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

URL 네임스페이스 추가하기

현재 튜토리얼에서는 polls라는 앱 하나만 있지만, 실제 장고 프로젝트는 여러개의 앱이 있을 수 있다. 장고가 {% url %} 태그를 사용할 때, 어떤 앱의 뷰에서 url를 생성할지 알수 있게하는 방법이 URLconf에 네임스페이스를 추가하는 것이다. polls/urls.pyapp_name을 추가하여 앱의 네임스페이스를 설정할 수 있다.

# polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

그리고 기존에 있던 polls/index.html 템플릿을 바꿔준다.

# polls/templates/polls/index.html

# before
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

# after
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

detail 뷰 구현하기


앞서 만들었던 폼은 투표 기능을 사용하는데 선택할 수 있는 폼이 아니라 단순한 문자를 출력하는 폼이였다. 투표는 선택이 가능해야하므로 질문에 라디오 버튼을 만들어야한다.

# polls/templates/polls/detail.html

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

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% 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 %}
<input type="submit" value="Vote">
</form>

forloop.counterfor 태그가 반복을 한 횟수를 나타낸다. POST를 사용하면 CSRF 공격에 대한 위험이 있기 때문에 이를 예방하기위해 {% csrf_token %} 태그를 사용한다. CSRF를 막기위해 로그인을 할 때마다 하나의 토큰을 발행하여 form태그의 POST 요청에 대해서만 이 토큰값이 유효한지 확인한다.

그럼 위처럼 Choice에 대한 라디오 버튼이 생기고, 하나를 선택한 뒤 Vote 버튼을 누르면 지난 시간에 만들었던 polls/views.pyvote 뷰 함수가 호출된다.

vote 뷰 구현하기

간단히 만들었던 polls/views.pyvote뷰를 제대로 구현해봐야 한다.

# polls/views.py

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 += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

vote뷰로 request 요청이 오면 request.POST['choice']을 이용해 선택된 결과를 들고 온다. 이때 request.POST['choice']에는 Choice 테이블의 id값이 들어있어 선택된 결과를 테이블에서 가져올 수 있다.
만약 선택을 하지 않을 경우 투표 폼을 "You didn't select a choice."문구와 함께 다시 보여준다.
선택을 했을 경우 투표 값을 올려주며 HttpResponseRedirect을 통해 results 뷰로 주소를 이동하도록 한다. POST로 값을 받은 경우 항상 HttpResponseRedirect로 반환한다. reverse() 함수는 resultsurl를 하드코딩 하지 않도록 /polls/3/results/로 반환해준다.

사실 vote뷰에서 사용한 selected_choice.votes += 1 selected_choice.save()는 문제가 있다. 두 명의 사용자가 동시에 값을 바꾸면 하나만 계산된다. 이 문제를 race condition 즉, 경쟁 상태라고 하는데 이에 대한 해결책이 있다.

result 뷰 구현하기

이제 result 뷰를 구현해야 한다.

# 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})

detail뷰와 거의 동일하다. 다만 여기서는 장고에서 지원하는 shortcuts모듈을 사용해서 간단히 표현했다.
detail 뷰에 대한 디자인도 해야한다. polls/templates/polls/results.html 파일을 생성한다.

# 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>

이제 투표를 한 결과를 보여주는 뷰를 완성했다.

Vote again?을 누르면 다시 투표할 수 있다.

제네릭 뷰(=클래스기반 뷰) 사용하기

사실 detail, vote, result 이 세가지 뷰는 거의 동일한 기능을 한다. request를 받으면 템플릿에 맞춰 데이터베이스에서 가져온 데이터를 리턴한다. 이런 일반적인 기능들은 대부분의 웹에서도 자주 사용하므로 장고에서는 제네릭 뷰를 지원하여 조금 더 빠르게 이러한 기능들을 구현하도록 도와준다. 코드도 굉장히 간결해진다.
제네릭 뷰는 GENERIC BASE View, GENERIC DETAIL View, GENERIC EDIT View 등 아주 다양한 뷰들을 지원하는데 우선 아래와 같은 뷰를 사용할 것이다. 제네릭 뷰에 대한 설명은 여기를 참고하면 된다.

  • DetailView
  • ListView

앞서 만들었던 뷰들을 제네릭 뷰로 바꾸기 위해 가장 먼저 URLconf를 수정해야 한다.

# 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'),
]

detailresult 뷰의 question_idpk로 바꾸었다. DetailViewurl에서 추출한 값이 기본 키 값이라고 생각하기 때문에 pk로 변경해야한다. as_view()함수를 통해 뷰를 호출함.

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.

polls/views.py를 제네릭뷰로 수정하면 위처럼 코드가 매우 간단해진다. 따로 데이터베이스에서 값을 로드하는 코드가 없이 model 속성을 사용하면 자동으로 Question 테이블을 가지고 온다. 또한 template_name 속성을 사용하면 render 이라는 메소드 없이 템플릿을 렌더링할 수 있다.

0개의 댓글