🌉 점프 투 장고 사이트를 참고하여 일정 단위의
chapter에 대해 실습 후새롭게 알게된 내용, 혹은복습이 꼭 필요한 내용에 대해 정리!!
2-04. 조회와 템플릿질문 목록 조회 구현http://localhost:8000/pybo
위의 페이지를 요청하면
현재 : "안녕하세요 pybo에 오신 것을 환영합니다." 라는 문구가 출력됨.
변경 : 질문 목록이 출력되도록 프로젝트 파일들을 수정!
📄 /pybo/views.py
# from django.http import HttpResponse # 삭제
from django.shortcuts import render
from .models import Question
def index(request):
# return HttpResponse("안녕하세요 pybo에 오신 것을 환영합니다.") # 삭제
question_list = Question.objects.order_by('-create_date')
context = {'question_list': question_list}
return render(request, 'pybo/question_list.html', context)
- 질문 목록 데이터 :
Question.objects.order_by('-create_date')
order_by: 조회 결과를 정렬하는 함수order_by('-create_date'): 작성일시를 역순으로 정렬하라는 의미-기호 : 붙어 있으면 역방향, 없으면 순방향 정렬을 의미
→ 게시물은 보통 최신순으로 보기 때문에 작성일시의 역순으로 정렬
render함수 :
- 파이썬 데이터를 템플릿에 적용하여 HTML로 반환하는 함수
- 즉, 위에서 사용한
render함수는질문 목록으로 조회한 question_list 데이터를pybo/question_list.html 파일에 적용하여HTML을 생성한후 리턴- 여기서 사용된
pybo/question_list.html과 같은 파일을템플릿(Template)이라고 부름.
→ 템플릿 파일은 HTML 파일과 비슷하지만 파이썬 데이터를 읽어서 사용할수 있는 HTML 파일
📄 /pybo/question_list.html
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="/pybo/{{ question.id }}/">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
{% if question_list %} 처럼 {% 와 %} 로 둘러싸인 문장들을 볼 수 있는데 이러한 것들을 템플릿 태그라고 함.question_list.html에 사용된 템플릿 태그들은 다음과 같음.question_list는 render 함수로 전달한 "질문 목록" 데이터views.py의 index 함수에서 2번째 줄에 있는 question_list임
http://localhost:8000/pybo 접속 시, 다음과 같이 변경된 화면을 확인 가능.

템플릿 태그
- 장고에서 사용하는 템플릿 태그는 다음 3가지 유형만 알면 됨.
- 파이썬의
if문과 유사하지만 항상{% endif %}태그로 닫아야 함.
{% if 조건문1 %}
<p>조건문1에 해당되는 경우</p>
{% elif 조건문2 %}
<p>조건문2에 해당되는 경우</p>
{% else %}
<p>조건문1, 2에 모두 해당되지 않는 경우</p>
{% endif %}
- 파이썬의
for문과 유사하지만 항상{% endfor %}태그로 닫아야 함.
{% for item in list %}
<p>순서: {{ forloop.counter }} </p>
<p>{{ item }}</p>
{% endfor %}
for 문 안에서는 다음과 같은 forloop 객체를 사용
- 파이썬의
for문과 유사하지만 항상{% endfor %}태그로 닫아야 함.
{{ 객체 }}
예) {{ item }}
예) {{question.id}}, {{question.subject}}
- 이 책에서는 파이보에 필요한 템플릿 문법이 나올 때마다 자세하게 알아볼 예정.
- 템플릿에 대한 보다 자세한 내용은 다음 URL을 참고.
https://docs.djangoproject.com/en/4.0/topics/templates/
질문 상세 조회 구현http://localhost:8000/pybo/2/
id 값이 2인 Question을 상세 조회동작을 위해 파일들을 아래와 같이 수정
📄 /pybo/urls.py
from django.urls import path
from pybo.views import index, detail
urlpatterns = [
path('', index),
path('<int:question_id>/', detail), # 추가
]
path('<int:question_id>/', detail)라는URL 매핑을 추가.- 이제
http://localhost:8000/pybo/2/페이지가 요청되면 여기에 등록한 매핑 룰에 의해http://localhost:8000/pybo/<int:question_id>/가 적용되어question_id에2가 저장되고detail함수도 실행될 것<int:question_id>에서int는 숫자가 매핑됨을 의미
📄 /pybo/views.py
detail 함수 추가def detail(request, question_id):
question = Question.objects.get(id=question_id)
context = {'question': question}
return render(request, 'pybo/question_detail.html', context)
detail함수 호출시 전달되는 매개변수가request외에question_id가 추가됨.- 매개변수
question_id에는URL매핑시 저장된question_id가 전달http://localhost:8000/pybo/2/페이지가 요청되면 매개변수question_id에2가 세팅되어detail함수가 실행
📄 `/templates/pybo/question_detail.html`
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
{{ question.subject }}과{{ question.content }}의question은detail함수에서 템플릿에context변수로 전달한Question 모델 객체
http://localhost:8000/pybo/2/ 접속

❗❗ 오류 페이지
http://localhost:8000/pybo/30/ 페이지를 요청

DoesNotExist 오류가 발생. question_id가 30이기 때문에 Question.object.get(id=30)이 호출되어 발생한 오류임.500이렇게 없는 데이터를 요청할 경우
500오류 페이지 보다는Not Found (404)페이지를 리턴하는 것이 바람직
[HTTP 주요 응답코드의 종류]

http://localhost:8000/pybo/30/ 처럼 없는 데이터를 요청할 경우 500 페이지 대신 404 페이지를 출력하도록 detail 함수를 수정
📄 /pybo/views.py
Question.objects.get(id=question_id)에서get함수를get_object_or_404(Question, pk=question_id)로 변경.
- 여기서 사용한
pk는Question모델의 기본키(Primary Key)에 해당하는 값을 의미
from django.shortcuts import render, get_object_or_404 # get_object_or_404 추가
from .models import Question
(... 생략 ...)
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id) # 수정
context = {'question': question}
return render(request, 'pybo/question_detail.html', context)
Generic View 알아두기
- 이 책은 제네릭뷰를 사용하지 않음.
- 왜냐하면 제네릭뷰는 매우 편리하지만 내부적으로 어떻게 동작하는지 이해하기 쉽지 않아 장고를 배우는 지금 이 시점에서는 오히려 혼란을 초래할 수 있기 때문
- 간단한 내용만 살펴볼 것
장고에는 제네릭뷰(Generic View)라는 것이 있음.
만약 우리가 views.py에 작성한 index나 detail 함수를 제네릭 뷰로 변경하면 다음처럼 간략하게 작성 가능
📄 /pybo/views.py
class IndexView(generic.ListView):
def get_queryset(self):
return Question.objects.order_by('-create_date')
class DetailView(generic.DetailView):
model = Question
IndexView클래스가index함수를 대체하고DetailView클래스가detail함수를 대체함.IndexView는 템플릿 명이 명시적으로 지정되지 않은 경우에는 디폴트로모델명_list.html을 템플릿명으로 사용.- 마찬가지로
DetailView는모델명_detail.html을 디폴트 템플릿명으로 사용.
pybo/urls.py 파일은 다음과 같이 변경되어야 함.📄 /pybo/urls.py
from django.urls import path
from . import views
app_name = 'pybo'
urlpatterns = [
path('', views.IndexView.as_view()),
path('<int:pk>/', views.DetailView.as_view()),
]
- 이렇듯 모델의 목록 조회나 상세 조회는 제네릭뷰를 사용하는것이 매우 간편함.
- 단, 제네릭 뷰를 사용할 경우에는 복잡한 케이스에서 더 어렵게 작성되는 경우가 종종 있으니 주의하여 사용
2-05. URL 별칭URL 하드코딩question_list.html 템플릿에 사용된 링크
<li><a href="/pybo/{{ question.id }}/">{{ question.subject }}</a></li>
위의 링크는 질문 상세를 위한 URL 링크임.
- 하지만 이러한 URL 링크는 수정될 가능성이 있음.
- 예를 들어,
http://localhost:8000/pybo/question/2또는http://localhost:8000/pybo/2/question처럼 변경될 가능성이 존재.- 실제 프로젝트에서
URL 리팩토링은 빈번하게 발생
URL 링크의 구조가 자주 변경된다면 템플릿에서 사용한 모든 URL들을 일일이 찾아가며 수정해야 하는 리스크가 발생.
이러한 문제점을 해결하기 위해서는 해당 URL에 대한 실제 링크 대신 링크의 주소가 1:1 매핑되어 있는 별칭을 사용
URL 별칭URL 매핑에 name 속성을 부여하면 됨./pybo.urls.py 파일을 다음과 같이 수정
- http://localhost:8000/pybo/ URL은
index,- http://localhost:8000/pybo/2와 같은 URL에는
detail라는 별칭을 부여한 것
📄 /pybo/urls.py
from django.urls import path
from pybo.views import index, detail
urlpatterns = [
path('', index, name='index'),
path('<int:question_id>/', detail, name='detail'),
]
템플릿에서 URL 별칭 사용하기pybo/urls.py 파일에 별칭을 추가하면 템플릿에서 다음처럼 사용
- 하드코딩 되어 있던
/pybo/{{ question.id }}링크를{% url 'detail' question.id %}로 변경- 여기서
question.id는URL 매핑에 정의된<int:question_id>에 전달해야 하는 값을 의미
📄 /templates/pybo/question_list.html
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="{% url 'detail' question.id %}">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
📌 파라미터명 전달
한 개의 파라미터를 전달할 경우에는 다음과 같이 사용{% url 'detail' question.id %}
파라미터 명을 함께 사용 가능{% url 'detail' question_id=question.id %}
2개 이상의 파라미터를 사용해야 한다면 다음과 같이 공백 문자 이후에 덧 붙여주면 됨.{% url 'detail' question_id=question.id page=2 %}
URL 네임스페이스
- 현재는
pybo앱 하나만 사용중이지만pybo앱 이외의 다른 앱이 프로젝트에 추가 될 수도 있을 것임.- 이런 경우 서로 다른 앱에서 동일한
URL별칭을 사용하면 중복이 발생할 것
❗❗ 이 문제를 해결하려면 pybo/urls.py 파일에 네임스페이스를 의미하는 app_name 변수를 지정해야 함.
app_name을pybo로 설정
📄 /pybo/urls.py
from django.urls import path
from pybo.views import index, detail
app_name = 'pybo' # 추가
urlpatterns = [
path('', index, name='index'),
path('<int:question_id>/', detail, name='detail'),
]
URL 별칭에 네임스페이스를 다음과 같이 지정해야 함.
detail앞에pybo라는 네임스페이스를 붙여준 것
📄 /templates/pybo/question_list.html
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a></li> # 수정
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
📌 redirect 함수와 URL 별칭
URL 별칭은 템플릿 외에 redirect 함수에서도 사용됨.redirect는 특정 페이지로 이동시키는 함수.redirect('pybo:detail', question_id=question.id)
2-06. 데이터 저장답변 등록 폼 추가form)을 추가
- 답변의 내용을 입력할 수 있는 텍스트창(
textarea)과 답변을 저장 할 수 있는 "답변등록" 버튼을 추가.- 답변 저장을 위한
URL은form 태그의action 속성에{% url 'pybo:answer_create' question.id %}로 지정
📄 /templates/pybo/question_list.html
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
<form action="{% url 'pybo:answer_create' question.id %}" method="post"> # 추가
{% csrf_token %} # 추가
<textarea name="content" id="content" rows="15"></textarea> # 추가
<input type="submit" value="답변등록"> # 추가
</form> # 추가
form 태그 바로 밑에 보이는 {% csrf_token %}은 보안에 관련된 항목으로 form으로 전송한 데이터가 실제 웹 페이지에서 작성한 데이터인지를 판단하는 가늠자 역할을 함.
- 만약 어떤 해커가 이상한 방법으로 데이터를 전송할 경우에는 서버에서 발행한
csrf_token값과 해커가 일방적으로 보낸csrf_token값이 일치하지 않기 때문에 블록킹될 것
form 태그 바로 밑에 {% csrf_token %} 태그를 항상 위치시켜야 함.
POST요청시form 태그에csrf_token이 없으면 장고는 오류를 냄.
CSRF란?CSRF(cross site request forgery)
CSRF 토큰 값을 세션을 통해 발행하고 웹 페이지에서는 폼 전송시에 해당 토큰을 함께 전송하여 실제 웹 페이지에서 작성된 데이터가 전달되는지를 검증하는 기술csrf_token 사용을 위해서는 CsrfViewMiddleware 미들웨어가 필요한데 이 미들웨어는 settings.py의 MIDDLEWARE 항목에 디폴트로 추가되어 있으므로 별도의 설정은 필요 없음.📄 /templates/pybo/question_list.html
(... 생략 ...)
MIDDLEWARE = [
(... 생략 ...)
'django.middleware.csrf.CsrfViewMiddleware',
(... 생략 ...)
]
(... 생략 ...)
csrf_token 기능을 사용하고 싶지 않다면 저 코드 한 줄을 주석처리하면 됨.URL 매핑pybo/urls.py에 다음과 같은 URL 매핑을 등록
answer_create별칭에 해당하는URL매핑 규칙을 등록.- 이제 http://locahost:8000/pybo/answer/create/2/ 와 같은 페이지를 요청하면
URL 매핑 규칙에 의해answer_create함수가 호출될 것
📄 /pybo/urls.py
from django.urls import path
from pybo.views import index, detail, answer_create # 추가
app_name = "pybo"
urlpatterns = [
path("", index),
path("<int:question_id>/", detail, name="detail"),
path("answer/create/<int:question_id>/", answer_create, name="answer_create"), # 추가
]
URL 매핑 규칙에 정의된 answer_create 함수를 pybo/views.py 파일에 다음처럼 추가
answer_create함수의 매개변수question_id는URL 매핑에 의해 그 값이 전달됨.- 만약 http://locahost:8000/pybo/answer/create/2/ 라는 페이지를 요청하면 매개변수
question_id에는2라는 값이 전달될 것
📄 /pybo/views.py
from django.shortcuts import render, get_object_or_404, redirect # 추가
from django.utils import timezone # 추가
from .models import Question
(... 생략 ...)
def answer_create(request, question_id):
question = get_object_or_404(Question, pk=question_id)
question.answer_set.create(content=request.POST.get('content'), create_date=timezone.now())
return redirect('pybo:detail', question_id=question.id)
답변 등록시 텍스트창에 입력한 내용은 answer_create 함수의 첫번째 매개변수인 request 객체를 통해 읽을 수 있음.
- 즉,
request.POST.get('content')로 텍스트창에 입력한 내용을 읽을 수 있음.request.POST.get('content'):POST로 전송된 폼(form) 데이터 항목 중content값을 의미
그리고 답변을 생성하기 위해 question.asnswer_set.create 를 사용.
question.answer_set: 질문의 답변을 의미.Question과Answer모델은 서로ForeignKey로 연결되어 있기 때문에 이처럼 사용 가능.
📌 답변을 저장하는 또 다른 방법은 Answer 모델을 직접 사용하는 방법
- 어떤것을 사용해도 결과는 동일
📄 /pybo/views.py
(... 생략 ...)
from .models import Question, Answer
(... 생략 ...)
def answer_create(request, question_id):
"""
pybo 답변등록
"""
question = get_object_or_404(Question, pk=question_id)
answer = Answer(question=question, content=request.POST.get('content'), create_date=timezone.now()) # 변경 및 추가된 부분
answer.save() # 변경 및 추가된 부분
return redirect('pybo:detail', question_id=question.id)
redirect 함수를 사용
redirect 함수: 페이지 이동을 위한 함수.pybo:detail별칭에 해당하는 페이지로 이동하기 위해redirect함수를 사용.- 그리고
pybo:detail별칭에 해당하는URL은question_id가 필요하므로question.id를 인수로 전달
답변 저장
- 텍스트 창에 아무 값이나 입력하고 답변을 등록해보면 화면에는 아무런 변화가 없음.
- 왜냐하면 아직 등록된 답변을 표시하는 기능을 템플릿에 추가하지 않았기 때문

답변 조회
- 중간 부분에 질문에 등록된 답변을 확인할 수 있는 영역을 추가.
question.answer_set.count: 답변의 총 갯수를 의미
question.answer_set: 질문과 연결된 답변들을 의미
📄 /templates/pybo/question_detail.html
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
<h5>{{ question.answer_set.count }}개의 답변이 있습니다.</h5> # 추가
<div> # 추가
<ul> # 추가
{% for answer in question.answer_set.all %} # 추가
<li>{{ answer.content }}</li> # 추가
{% endfor %} # 추가
</ul> # 추가
</div> # 추가
<form action="{% url 'pybo:answer_create' question.id %}" method="post">
{% csrf_token %}
<textarea name="content" id="content" rows="15"></textarea>
<input type="submit" value="답변등록">
</form>
