https://docs.djangoproject.com/ko/5.1/intro/tutorial04/#
이제까지 다뤘던 내용을 정리해보면
장고는 MTV(Model - Template - View)디자인 패턴으로 역할에 따라 코드를 분리하는 가이드로 사용되고 있습니다.
이 패턴은 소프트웨어 디자인 패턴인 MVC(Model-View-Controller)와 같은 패턴이며 부르는 명칭에만 차이가 있습니다.
MVC의 View는 MTV의 Template
MVC의 Controller는 MTV의 View
Model
모델(Model)은 장고 데이터베이스를 연결시켜주는 코드이고 데이터의 형태를 나타냅니다. 각각 모델은 데이터베이스 테이블과 매핑됩니다.
모든 모델 클래스는 django.db.models.Model 파이썬 클래스를 상속받고 각각의 모델 속성은 필드로 나타냅니다. 파일명은 models.py
Template
템플릿(Template)은 클라이언트에게 제공될 결과물로 HTML을 사용해서 나타내고 장고에서는 template 디렉터리내에서 HTML 파일을 사용합니다.
View
뷰(View)는 사용자 요청을 받아 처리하는 로직으로 파이썬 함수를 사용합니다. 파일명은 views.py
1. 간단한 폼(Form) 작성하기
웹 어플리케이션에서 form은 클라이언트로부터 데이터를 입력받아 서버로 전송하는 핵심적인 인터페이스입니다.
이번 챕터를 통해서 아래와 같이 기본적인 데이터 흐름에 대해 익히는 것을 목표로 하면 좋겠습니다.
- 장고 템플릿에서 사용자가 입력을 어떻게 받고(투표 선택)
- 서버에서 어떻게 처리하고 투표수(votes)증가
- 결과를 어떻게 저장하고(DB 업데이트)
- 응답을 어떻게 보여줄지 (결과 페이지로 리다이렉트)
- CSRF토큰 보안 개념 익히기
1.1 Form 태그
투표 상세 템플릿을 수정하여 템플릿 HTML에 사용자 입력을 받는 <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은 input 입력란, 체크박스, 라디오 버튼, 드롭다운 등 다양한 입력 요소를 포함할 수 있고 이 데이터를 서버로 전송하는 역할을 해줍니다.
<form action="/login" method="post">
<!-- 로그인 입력 받기 -->
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">로그인</button>
</form>
1.2 fieldset HTML 태그
<fieldset> 요소는 <form>태그의 요소들을 그룹화하는 컨테이너입니다. 주로 input 입력 필드들을 그룹화하는데 사용됩니다.
<form>
<fieldset>
<legend>개인정보</legend>
<input type="text" name="name">
<input type="email" name="email">
</fieldset>
<fieldset>
<legend>배송정보</legend>
<input type="text" name="address">
<input type="tel" name="phone">
</fieldset>
</form>
<legend>요소는 그룹핑 타이틀입니다. 스크린샷에서 [What's New] 영역입니다.
1.3 Form 데이터 제출시 데이터 형태
사용자가 <input type="radio" name="choice" value="{{ choice.id }}"> 중 choice.id가 3인 항목 하나를 선택하고 폼을 제출했다면,
name="choice"는 키(key)
value="{{ choice.id }}"은 choice에 해댱되는 value로 요청 데이터는 'choice=3' 의 형태로 서버에 전달됩니다.
1.3.1 장고의 request.POST
# polls/views.py
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']) # html input name 속성
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
"question": question,
"error_message": "Please select a choice",
},)
request.POST['choice']는 전달된 POST 요청 데이터에서 "choice"라는 키를 찾아서 그 값을 가져옵니다.
사용자가 선택한 값이 3이라면 request.POST['choice']는 '3'을 반환합니다.
1.3.2 CSRF 보호
템플릿 코드에서 {% csrf_token %}를 발견하셨을텐데요. 장고에서 보안 세팅까지 해둔 기능입니다.
(1) CSRF 토큰이란?
CSRF 토큰은 서버가 사용자에게 발급하는 고유한 값으로 각 요청마다 유효한 CSRF 토큰을 보내야합니다.
서버는 요청이 실제 사용자의 의도에서 발생한 것인지 확인하기 위해, 요청과 함께 보내진 CSRF 토큰의 값을 확인합니다.
만약 이 값이 다르다면 요청은 악의적인 요청으로 간주하고 거부됩니다.
(2) CSRF 방어 메커니즘
장고는 기본적으로 모든 POST 요청에 대해 CSRF 보호를 활성화합니다. 이를 통해 사용자가 의도하지 않은 요청이 서버로 전송되는 것을 방지할 수 있습니다.
(3) CSRF 토큰 적용 방법
장고에서 CSRF 토큰을 사용하는 방법은 POST 요청을 보낼 때 form 태크내에 {% csrf_token %} 템플릿 태그를 감싸주면 됩니다.
1.4 '모델명_set' - Django ORM 정방향,역방향 관계
1.4.1 외래키(ForeignKey)
개념에 들어가기 전에 외래키에 대해 알아보겠습니다.
외래키는 데이터베이스에서 두 테이블 간의 관계를 정의하는 필드로 한모델이 다른 모델에 속하거나, 참조하는 관계를 나타냅니다.
장고에서는 외래키를 통해 두 모델 간의 다대일 관계를 설정하며, 참조 대상 데이터가 삭제될 경우의 동작도 함께 정의합니다.
예를 들어 댓글 기능이 있는 블로그 기능을 만든다면 블로그 글에 댓글이 있어야 의미가 있지요.
이럴때 댓글 모델에 FK를 추가해서 댓글이 어떤 블로그 글에 속하는지 연결합니다.
댓글은 특정 블로그 글에 달려야 하므로 Comment 모델이 BlogPost 모델에 종속됩니다.
class BlogPost(models.Model):
title = models.CharField(max_length=200)
class Comment(models.Model):
content = models.TextField()
blog_post = models.ForeignKey(BlogPost, on_delete=models.CASCADE)
1.4.2 '모델명_set'
장고에서는 related_name을 지정하지 않았을 때, 기본적으로 외래키(FK)를 통해 연결된 모델의 데이터를 참조하기 위해 사용되는 기본 명명 규칙이 '모델명_set'입니다.
이 규칙은 역참조(reverse relationship)을 위한 것입니다.
템플릿 Form에서 {% for choice in question.choice_set.all %}는 현재 한 질문에 연결된 모든 선택지 데이터를 반복문으로 가져오는 코드입니다.
choice_set은 특정 질문(Question)에 연결된 여러 선택지들을(Choice)을 간단히 불러올 수 있습니다.
choice_set은 ForeignKey를 통해 연결된 데이터를 역방향 조회로 쉽게 가져올 수 있도록 장고가 자동으로 만들어주는 도구입니다.
장고 ORM에서는 모델 간의 참조 관계를 정의하거나 조회할 때, 정방향과 역방향 두가지로 접근할 수 있습니다.
(1) Forward Relationship(정방향관계): 한 모델에서 다른 모델의 데이터를 참조할 때 사용하는 관계입니다. 기본적인 구조죠.
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
이렇게 정의된 question 필드를 사용해서 데이터를 조회합니다.
choice.question # Choice가 참조하고 있는 Question 객체를 가져옴
(2) Reverse Relationship(역방향 관계)
역방향 관계는 ForeignKey를 설정하지 않은 모델에서도 관련 데이터를 쉽게 가져올 수 있게 해주는 Django의 기능입니다.
정리하자면 ForeignKey를 정의한 모델뿐만 아니라 연결된 다른 모델에서도 데이터를 조회할 수 있도록 자동으로 만들어진 도구라고 보면 됩니다. 이게 뭔말인데 싶으실텐데..
위에 정방향 관계 개념에서 아래와 같이 FK를 정의한 모델 클래스가 있었죠
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
Choice 모델은 Question 모델과 FK로 연결되어 있습니다. 따라서 Choice에서 choice.question으로 연결된 Question 데이터를 조회 할 수 있었습니다.
역방향 관계의 경우는 FK를 정의하지 않은 Question모델에서도 Answer 데이터를 가져올 수 있는 기능을 제공합니다.
이때 장고에서 자동 생성되는 역방향 관계 매니저입니다.
question.choice_set.all()을 통해 연결된 모든 Choice 객체를 가져올 수 있는 기능을 제공합니다.
1.5 투표 기능을 위한 vote URL 등록
투표 기능 페이지 등록을 위해 url을 정의합니다.
페이지 url은 question id를 받고 /vote/로 연결을 합니다. (e.g. polls/1/vote/) views.vote 뷰함수로 기능 정의가 되었습니다.
# polls/urls.py
path("<int:question_id>/vote/", views.vote, name="vote"),
1.6 투표기능 vote view함수 정의
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()
# 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,)))
1.6.1 get_object_or_404
question = get_object_or_404(Question, pk=question_id)
question_id에 해당하는 Question 객체를 데이터베이스에서 찾습니다. 만약 해당 Question이 없으면 404 오류 페이지를 자동으로 반환합니다.
1.6.2 사용자가 선택한 choice항목 처리
selected_choice = question.choice_set.get(pk=request.POST["choice"])
사용자가 선택한 항목의 choice값을 POST 요청에서 받아옵니다. 이 값은 라디오 버튼에서 선택된 choice.id입니다.
question.choice_set.get()에서 사용자가 선택한 choice.id에 해당되는 Choice 모델에서 question과 연결된 Choice 항목을 가져옵니다.
1.6.3 예외 처리
사용자가 선택하지 않았거나 잘못된 선택이 이루어진 경우 KeyError나 Choice.DoesNotExist로 예외처리를 합니다.
except (KeyError, Choice.DoesNotExist):
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
저도 예외처리를 설계하는게 아직 익숙치 않아서 왜 공식문서에서는 이렇게 설계했을까 chat gpt에 물어보니
<GPT 답변>
일반적으로는 선택지가 없는 상태에서 제출 버튼을 누르지 않겠지만, 웹 애플리케이션에서는 항상 사용자가 의도하지 않은 행동을 하거나 비정상적인 요청을 보낼 가능성을 고려해야 합니다. (= 너의 상식이 유저도 같을거라고 믿지마인듯)
(1) KeyError:
• 이 예외는 request.POST['choice']에서 choice라는 key가 없는 경우 발생합니다. (input에 name속성이 choice가 아니면 발생하는 에러겠네요)
이는 다음과 같은 상황에서 발생할 수 있습니다:
• HTML에서 선택지 버튼을 클릭하지 않고 폼을 제출한 경우.
• 클라이언트가 의도적으로 choice 데이터를 빼고 잘못된 요청을 보낸 경우.
• 즉, 서버에서 폼 데이터가 불완전한 요청을 받을 때 발생합니다.
(2) Choice.DoesNotExist:
• 이 예외는 question.choice_set.get(pk=request.POST['choice'])에서 발생하며, request.POST['choice']에 담긴 값에 해당하는 Choice가 데이터베이스에 없을 때 발생합니다.
이런 경우는 다음과 같은 상황에서 발생할 수 있습니다:
• 사용자가 비정상적인 방법으로 요청을 조작하여 존재하지 않는 pk 값을 전송한 경우.
• 선택 항목이 삭제되었지만, 클라이언트는 캐시된 폼 데이터를 전송한 경우.
• 즉, 데이터베이스에 실제로 존재하지 않는 선택지를 처리하는 상황입니다.
사용자가 선택지가 없는 상태에서 제출할 수 있는 경우👽!
• 사용자가 라디오 버튼을 선택하지 않고 제출했을 때 (KeyError 발생 가능).
• 악의적인 사용자나 스크립트가 조작된 데이터를 보냈을 때(Choice.DoesNotExist 발생 가능).
• 선택지가 삭제되었는데 사용자의 폼이 캐시된 상태에서 제출되었을 때(Choice.DoesNotExist 발생 가능).
왜 굳이 예외 처리를 해야 할까?
웹 애플리케이션의 안정성과 보안을 위해 예외 처리는 중요합니다.
• 안정성: 예상치 못한 데이터로 인해 서버가 중단되지 않도록 방지합니다.
• 보안: 의도적으로 조작된 데이터로 서버가 잘못된 동작을 하지 않도록 막습니다.
결론
비정상적인 상황에서도 애플리케이션이 중단되지 않고, 사용자에게 적절한 메시지(예: “선택지를 선택해 주세요”)를 보여주며 대응하기 위해 예외 처리를 하는 것이 Django에서 권장되는 방식입니다. 예외적인 상황은 정상적인 상황에서 발생하지 않더라도 항상 방어적으로 대비하는 것이 좋습니다.
1.6.4 선택된 choice 항목에 투표 수 증가
F() 함수는 장고 ORM에서 제공하는 기능 중 하나로 DB수준에서 필드 값을 동적으로 계산하는 기능을 제공합니다.
파이썬 코드내에서 실행되는 코드는 아니고, 서버와 데이터베이스에 해당 연산을 위임합니다.
selected_choice.votes = F("votes") + 1
selected_choice.save()
1.6.5 HttpResponseRedirect 사용
투표가 성공적으로 처리된 후, 사용자에게 결과 페이지로 리다이렉트합니다.
리다이렉트는 사용자가 새로운 URL로 이동하게 지시하는데요, 왜 사용하냐면 중복된 POST 요청을 방지를 할 수 있습니다.
만약 사용자가 투표 제출 후에 페이지 뒤로가기를 해서 다시 투표를 제출하면 중복 투표가 집계되겠죠.
그래서 새로 로드하여서 중복 POST 요청을 방지합니다.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
(1) reverse 함수: 장고가 제공하는 함수로 URL이름을 이용해 실제 URL을 생성합니다.
URL이름은 앱 urls.py에서 정의했던 name space와 url 별칭입니다.
# revers 함수 사용
from django.urls import path
from . import views
app_name = "polls" # 네임스페이스 정의
urlpatterns = [
path("<int:question_id>/results/", views.results, name="results"), # 'polls:results'
]
이렇게 되면 'polls:result' url 이름으로 url을 동적으로 생성할 수 있죠
<!-- 템플릿에서 사용 -->
<a href="{% url 'polls:results' question.id %}">결과 보기</a>
동적 url 이점은 URL관리가 편하고, 하드코딩 방지, 네임스페이 충돌 예방이 가능합니다.
1.7 투표 결과 기능 작성하기 - result.html
이제 리다이렉트된 결과 페이지 템플릿을 작성하면 됩니다.
/polls/1/ 페이지에 가서 투표를 하면 반영된 결과 페이지를 볼 수 있습니다.
에러처리도 잘 처리됐는지 확인하려면 선택항목을 선택하지 않고 투표를 제출해보는 것도 좋겠네요.
<!-- 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>
제네릭뷰는 다음편에..
'Programming👩🏻💻 > Web framework' 카테고리의 다른 글
[FastAPI] DDD (Domain-Driven Design) pattern (0) | 2024.12.14 |
---|---|
[Django] 공식문서로 익혀보기 part4.2 - 함수/클래스뷰, 제네릭뷰 (1) | 2024.11.30 |
[Django] 공식문서로 익혀보기 part3 - views 작성 (2) | 2024.11.19 |
[Django] 공식문서로 익혀보기 part2 - 데이터베이스 설치 (6) | 2024.11.15 |
[Django] 공식문서로 익혀보기 part1 - 첫 시작, 프로젝트와 앱 만들기 (5) | 2024.11.15 |