🌉 점프 투 장고 사이트를 참고하여 일정 단위의
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>