221009_점프투장고 2-04 ~ 2-06

Csw·2022년 10월 9일
0

Django

목록 보기
10/14

🌉 점프 투 장고 사이트를 참고하여 일정 단위의 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)
  • 코드 풀이
    1. 질문 목록 데이터 :
      Question.objects.order_by('-create_date')
      • order_by : 조회 결과를 정렬하는 함수
      • order_by('-create_date') : 작성일시를 역순으로 정렬하라는 의미
      • - 기호 : 붙어 있으면 역방향, 없으면 순방향 정렬을 의미
        → 게시물은 보통 최신순으로 보기 때문에 작성일시의 역순으로 정렬

    2. 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_listrender 함수로 전달한 "질문 목록" 데이터
      views.pyindex 함수에서 2번째 줄에 있는 question_list

http://localhost:8000/pybo 접속 시, 다음과 같이 변경된 화면을 확인 가능.

🌈 템플릿 태그

  • 장고에서 사용하는 템플릿 태그는 다음 3가지 유형만 알면 됨.

🚦 1. 분기

  • 분기문 태그의 사용법
    • 파이썬의 if 문과 유사하지만 항상 {% endif %} 태그로 닫아야 함.
{% if 조건문1 %}
    <p>조건문1에 해당되는 경우</p>
{% elif 조건문2 %}
    <p>조건문2에 해당되는 경우</p>
{% else %}
    <p>조건문1, 2에 모두 해당되지 않는 경우</p>
{% endif %}

🚦 2. 반복

  • 반복문 태그의 사용법
    • 파이썬의 for 문과 유사하지만 항상 {% endfor %} 태그로 닫아야 함.
{% for item in list %}
    <p>순서: {{ forloop.counter }} </p>
    <p>{{ item }}</p>
{% endfor %}
  • 템플릿 for 문 안에서는 다음과 같은 forloop 객체를 사용

🚦 3. 객체 출력

  • 객체를 출력하기 위한 태그의 사용법
    • 파이썬의 for 문과 유사하지만 항상 {% endfor %} 태그로 닫아야 함.
{{ 객체 }}

예) {{ item }}

  • 객체에 속성이 있는 경우는 파이썬과 동일한 방법으로 도트(.) 문자를 이용하여 표시

예) {{question.id}}, {{question.subject}}

🌈 질문 상세 조회 구현

http://localhost:8000/pybo/2/

  • 이 URL의 의도 : 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_id2가 저장되고 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_id2가 세팅되어 detail 함수가 실행

📄 `/templates/pybo/question_detail.html`

  • detail 함수에서 사용할 pybo/question_detail.html 템플릿
<h1>{{ question.subject }}</h1>
<div>
    {{ question.content }}
</div>
  • {{ question.subject }}{{ question.content }}questiondetail 함수에서 템플릿에 context 변수로 전달한 Question 모델 객체

http://localhost:8000/pybo/2/ 접속

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

  • 그러면 DoesNotExist 오류가 발생.
  • 이 오류는 전달된 question_id30이기 때문에 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)로 변경.
    • 여기서 사용한 pkQuestion 모델의 기본키(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에 작성한 indexdetail 함수를 제네릭 뷰로 변경하면 다음처럼 간략하게 작성 가능

📄 /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 별칭

📄 /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.idURL 매핑에 정의된 <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 %}

📌 파라미터명 전달

  1. 한 개의 파라미터를 전달할 경우에는 다음과 같이 사용
{% url 'detail' question.id %}
  1. 이 때 다음처럼 파라미터 명을 함께 사용 가능
{% url 'detail' question_id=question.id %}
  1. 만약 2개 이상의 파라미터를 사용해야 한다면 다음과 같이 공백 문자 이후에 덧 붙여주면 됨.
{% url 'detail' question_id=question.id page=2 %}

🌈 URL 네임스페이스

  • 현재는 pybo 앱 하나만 사용중이지만 pybo 앱 이외의 다른 앱이 프로젝트에 추가 될 수도 있을 것임.
  • 이런 경우 서로 다른 앱에서 동일한 URL 별칭을 사용하면 중복이 발생할 것

❗❗ 이 문제를 해결하려면 pybo/urls.py 파일에 네임스페이스를 의미하는 app_name 변수를 지정해야 함.

  • app_namepybo로 설정

📄 /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)과 답변을 저장 할 수 있는 "답변등록" 버튼을 추가.
    • 답변 저장을 위한 URLform 태그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.pyMIDDLEWARE 항목에 디폴트로 추가되어 있으므로 별도의 설정은 필요 없음.

📄 /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_idURL 매핑에 의해 그 값이 전달됨.
    • 만약 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 : 질문의 답변을 의미.
    • QuestionAnswer 모델은 서로 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 별칭에 해당하는 URLquestion_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>
  • 이렇게 수정 후, 다시 질문 상세 화면을 호출

0개의 댓글