🌉 점프 투 장고 사이트를 참고하여 일정 단위의
chapter에 대해 실습 후새롭게 알게된 내용, 혹은복습이 꼭 필요한 내용에 대해 정리!!
2-07. 스태틱템플릿에 스타일 적용
html문서 상단에 하단의 코드 추가- 템플릿에 스타일시트와 같은 스태틱 파일을 사용하기 위해서는 템플릿 최상단에
{% load static %}태그를 먼저 삽입해야 함.- 그래야만
{% static ... %}와 같은 템플릿 태그를 사용 가능.
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
2-08. 부트스트랩
- 부트스트랩(
Bootstrap)은 디자이너의 도움 없이도 개발자 혼자서 상당히 괜찮은 수준의 웹 페이지를 만들수 있게 도와주는 프레임워크.- 부트스트랩은 트위터(
오픈소스프로젝트
부트스트랩 설치부트스트랩 주의사항3.x, 4.x, 5.x 등의 버전이 존재하고 메이저 번호(3, 4, 5)에 따라 그 사용방법이 다름.
bootstrap.min.css 파일과 bootstrap.min.js 파일을 카피하여 static 디렉터리에 저장부트스트랩 적용질문 목록 template에 부트스트랩을 다음처럼 적용📄 /templates/pybo/question_list.html
{% load static %} # 추가
<link rel="stylesheet" type="text/css" href="{% static bootstrap.min.css' %}"> # 추가
{% if question_list %}
(... 생략 ...)
style을 적용 했으니 template도 부트스트랩을 사용하도록 다음과 같이 수정
- 기존에는
<ul>태그로 심플하게 작성했던 질문 목록을테이블 구조로 변경.- 그리고 번호와 작성일시 항목도 추가함.
- 번호는
for문의 현재 순서를 의미하는{{ forloop.counter }}를 이용.- 여기서 사용한
class="container my-3",class="table",class="table-dark"등은 부트스트랩 스타일에 정의되어 있는 클래스들.
📄 /templates/pybo/question_list.html
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<div class="container my-3">
<table class="table">
<thead>
<tr class="table-dark">
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
{% if question_list %}
{% for question in question_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>
<a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a>
</td>
<td>{{ question.create_date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">질문이 없습니다.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
📌 부트스트랩에 대한 자세한 내용은 다음 URL을 참조!!!
- 앞으로 템플릿 작성시에 계속 부트스트랩 스타일들을 사용할 것
https://getbootstrap.com/docs/5.1/getting-started/introduction/
이제 다음처럼 부트스트랩이 적용된 질문 목록을 볼 수 있을

이어서 질문 상세 template에도 다음처럼 부트스트랩을 적용
- 이번에는 수정사항이 좀 많음.
- 부트스트랩으로 화면을 구성하다 보면 가끔은 이렇게 많은 양의
HTML코드를 작성해야 함.질문이나답변은하나의 뭉치에 해당하므로 부트스트랩의card 컴포넌트를 사용.
📄 /templates/pybo/question_detail.html
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<div class="container my-3">
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2">
{{ question.create_date }}
</div>
</div>
</div>
</div>
<!-- 답변 -->
<h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>
{% for answer in question.answer_set.all %}
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2">
{{ answer.create_date }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 답변 등록 -->
<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
{% csrf_token %}
<div class="mb-3">
<label for="content" class="form-label">답변내용</label>
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
</form>
</div>
부트스트랩 card 컴포넌트 :
질문 상세 템플릿에 사용한 부트스트랩 클래스
- 그리고 질문 내용과 답변 내용에는
style="white-space: pre-line;"과 같은 스타일을 지정해 줌.- 글 내용의 줄 바꿈을 정상적으로 표시하기위해 적용한 스타일임.
부트스트랩을 적용한 질문 상세 화면

2-09. 템플릿 상속표준 HTML 구조
- 표준
HTML문서의 구조는 위의 예처럼html,head,body엘리먼트가 있어야 하며,CSS파일 링크는head엘리먼트 안에 있어야 함.
또한head엘리먼트 안에는meta,title엘리먼트 등이 포함되어야 함.
📄 [표준 HTML 구조의 예]
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
<title>Hello, pybo!</title>
</head>
<body>
(... 생략 ...)
</body>
</html>
<table> (... 생략 ...) </table> <!-- table 엘리먼트 -->
위에서 <table>은 table 태그이고 <table> ~ </table> 처럼 table 태그로 시작해서 table 태그로 닫힌 구간(Block)은 table 엘리먼트이다.
템플릿 상속HTML 구조가 되도록 수정하는 방법은 무엇일까?
- 그런데 템플릿 파일들을 모두 표준
HTML구조로 변경하면body엘리먼트 바깥 부분(head엘리먼트 등)은 모두 같은 내용으로 중복될 것임.- 그러면
CSS파일 이름이 변경되거나 새로운CSS파일이 추가될 때마다 모든 템플릿 파일을 일일이 수정해야 함.
템플릿 상속(extend) 기능을 제공함.템플릿 상속 : 기본 틀이 되는 템플릿을 먼저 작성하고 다른 템플릿에서 그 템플릿을 상속해 사용하는 방법base.html 템플릿
- 모든 템플릿이 상속해야 하는 템플릿
- 표준
HTML문서의 기본 틀body엘리먼트 안의{% block content %}와{% endblock %}템플릿 태그는base.html을상속한 템플릿에서 개별적으로 구현해야 하는 영역이 됨.
📄 /templates/base.html
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
<!-- pybo CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
<title>Hello, pybo!</title>
</head>
<body>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
question_list 템플릿question_list.html 템플릿을 다음과 같이 변경
base.html템플릿을 상속하기 위해{% extends 'base.html' %}처럼extends템플릿 문법을 사용.- 상단의 두 줄은
base.html에 이미 있는 내용이므로 삭제.- 그리고
{% block content %}와{% endblock %}사이에question_list.html에서만 쓰이는 내용을 작성.- 이렇게 하면 이제
question_list.html은base.html템플릿을 상속받아 표준HTML문서로 바뀌게 됨.
📄 /templates/pybo/question_list.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<table class="table">
(... 생략 ...)
</table>
</div>
{% endblock %}
question_detail 템플릿question_detail.html 템플릿을 다음과 같이 변경
{% extends 'base.html' %}템플릿 태그를 맨 위에 추가하고 기존 내용 위 아래로{% block content %}와{% endblock %}를 작성.- 템플릿 상속을 적용한 후 질문 목록, 질문 상세를 조회해보면 화면에 보여지는 것은 동일하지만 표준 HTML 구조로 변경된 것을 확인할 수 있음.
📄 /templates/pybo/question_list.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
(... 생략 ...)
</form>
</div>
{% endblock %}
2-10. 폼질문 등록html 파일에 질문 등록하기 버튼 만들기
<a href="...">과 같은 링크이지만 부트스트랩의btn btn-primary클래스를 적용하면 버튼으로 보이게 됨.- 버튼을 클릭하면
pybo:question_create별칭에 해당되는URL이 호출될 것
📄 /templates/pybo/question_list.html
(... 생략 ...)
</table>
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a> # 추가
</div>
{% endblock %}
URL 매핑pybo:question_create 별칭에 해당되는 URL 매핑 규칙을 추가
question_create함수를 호출하도록 매핑
📄 /pybo/urls.py
from pybo.views import index, detail, question_create, answer_create # 추가
(... 생략 ...)
urlpatterns = [
(... 생략 ...)
path('question/create/', question_create, name='question_create'), # 추가
]
폼views.py에서 question_create 함수를 작성하기 전에 폼(Form)을 작성
- 폼(
Form)은 페이지 요청시 전달되는 파라미터들을 쉽게 관리하기 위해 사용하는 클래스- 폼은 필수 파라미터의 값이 누락되지 않았는지, 파라미터의 형식은 적절한지 등을 검증할 목적으로 사용
- 이 외에도 HTML을 자동으로 생성하거나 폼에 연결된 모델을 이용하여 데이터를 저장하는 기능도 있음.
question_create함수를 호출하도록 매핑
질문 등록시 사용할 QuestionForm을 forms.py 파일에 작성
QuestionForm은 모델 폼(forms.ModelForm)을 상속- 장고의 폼은 일반 폼(
forms.Form)과 모델 폼(forms.ModelForm)이 있음.
- 모델 폼(
forms.ModelForm) : 모델(Model)과 연결된 폼으로 폼을 저장하면 연결된 모델의 데이터를 저장할수 있는 폼
👉 모델 폼은 이너 클래스인Meta클래스가 반드시 필요
👉Meta클래스에는 사용할 모델과 모델의 속성을 기재해야 함.
즉, QuestionForm은 Question 모델과 연결된 폼이고 속성으로 Question 모델의 subject와 content를 사용한다고 정의한 것
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
view 함수views.py에서 question_create 함수를 작성
question_create함수는 위에서 작성한QuestionForm을 사용render함수에 전달한{'form': form}은 템플릿에서 질문 등록시 사용할 폼 엘리먼트를 생성할 때 사용
📄 /pybo/forms.py
from .forms import QuestionForm
(... 생략 ...)
def question_create(request):
form = QuestionForm()
return render(request, 'pybo/question_form.html', {'form': form})
templatepybo/question_form.html 템플릿을 작성
- 템플릿에서 사용한
{{ form.as_p }}의form은question_create함수에서 전달한QuestionForm의 객체{{ form.as_p }}는 폼에 정의한subject,content속성에 해당하는HTML코드를 자동으로 생성
📄 /template/pybo/question_form.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
action 속성보통 form 태그에는 항상 action 속성을 지정하여 submit 실행시 action에 정의된 URL로 폼을 전송해야 함.
- 하지만 여기서는 특별하게
action속성을 지정하지 않았음.form태그에action속성을 지정하지 않으면 현재 페이지의URL이 디폴트action으로 설정됨.
물론 action 속성을 다음처럼 명확하게 지정해도 됨.
<form method="post" action="{% url 'pybo:question_create' %}">
question_form.html 템플릿은 질문 등록 에서만 사용 가능
- 이후에 진행할
질문 수정에서는 이 템플릿을 활용할 수가 없음.- 왜냐하면
질문 수정일 경우에는action값을 달리해야 하기 때문- 동일한 템플릿을 여러 기능에서 함께 사용할 경우에는 이처럼
form의action속성을 비워두는 트릭을 종종 사용- 우리는 이후에
질문 수정기능을 구현할 때도question_form.html템플릿을 사용할 것이므로action속성은 비워둘 것!!
GET과 POST서버 재시작 후 브라우저에서 정상 작동 여부 확인
- forms.py와 같은 신규 파일 작성시에는 로컬 서버 재시작이 필요

질문 목록 화면 하단에 "질문 등록하기" 버튼이 추가됨.

question_create 함수에 데이터를 저장하는 코드를 작성
📄 /pybo/views.py
def question_create(request):
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid(): # 폼이 유효하다면
question = form.save(commit=False) # 임시 저장하여 question 객체를 리턴받고
question.create_date = timezone.now() # 실제 저장을 위해 작성일시를 설정.
question.save() # 데이터를 실제로 저장.
return redirect('pybo:index')
else:
form = QuestionForm()
context = {'form': form}
return render(request, 'pybo/question_form.html', context)
POST, GET동일한 URL 요청을 POST, GET 요청 방식에 따라 다르게 처리
- 질문 목록 화면에서 "질문 등록하기" 버튼을 클릭한 경우에는
/pybo/question/create/페이지가GET방식으로 요청되어question_create함수가 실행됨.
- 왜냐하면
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>와 같이 링크를 통해 페이지를 요청할 경우에는 무조건GET방식이 사용되기 때문.
따라서 이 경우에는request.method값이GET이 되어if .. else ..구문에서else구문을 타게 되어 질문을 등록하는 화면을 렌더링
- 그리고 질문 등록 화면에서
subject,content항목에 값을 기입하고저장하기버튼을 누르면 이번에는/pybo/question/create/페이지를POST방식으로 요청함.
- 왜냐하면 앞서 설명했듯이
form태그에action속성이 지정되지 않으면 현재 페이지가 디폴트action으로 설정되기 때문.- 따라서 질문 등록 화면에서
저장하기버튼을 클릭하면question_create함수가 실행되고request.method값은POST가 되어 해당 코드 블럭이 실행됨.
GET 방식에서는 form = QuestionForm() 처럼 QuestionForm을 인수 없이 생성.
POST 방식에서는 form = QuestionForm(request.POST) 처럼 request.POST를 인수로 생성.
request.POST를 인수로QuestionForm을 생성할 경우에는request.POST에 담긴subject,content값이QuestionForm의subject,content속성에 자동으로 저장되어 객체가 생성됨.
request.POST에는 화면에서 사용자가 입력한 내용들이 담겨있음.- 그리고
form.is_valid()는form이 유효한지를 검사
- 만약
form에 저장된subject,content의 값이 올바르지 않다면form에는 오류 메시지가 저장되고form.is_valid()가 실패하여 다시 질문 등록 화면을 렌더링.- 이 때 form에는 오류 메시지가 저장되므로 화면에 오류를 표시할 수 있음.
if form.is_valid(): form이 유효하다면 이후의 문장이 수행되어 질문 데이터가 생됨.
question = form.save(commit=False) : form에 저장된 데이터로 Question 데이터를 저장하기 위한 코드
QuestionForm이Question모델과 연결된 모델 폼이기 때문에 이와 같이 사용 가능.- 여기서
commit=False는 임시 저장을 의미.
- 즉, 실제 데이터는 아직 데이터베이스에 저장되지 않은 상태를 뜻함.
- 여기서
form.save(commit=False)대신form.save()를 수행하면Question모델의create_date에 값이 없다는 오류가 발생.- 왜냐하면
QuestionForm에는 현재subject,content속성만 정의되어 있고create_date속성은 없기 때문.- 이러한 이유로 임시 저장을 하여 question 객체를 리턴받고 create_date에 값을 설정한 후 question.save()로 실제 데이터를 저장하는 것
create_date속성은 데이터 저장 시점에 생성해야 하는 값이므로 QuestionForm에 등록하여 사용하지 않음.
이제 브라우저에서 질문 등록이 잘 되는지 확인



form widget{{ form.as_p }} 태그는 HTML 코드를 자동으로 생성하기 때문에 부트스트랩을 적용할 수가 없음.
- 완벽하지는 않지만 다음처럼
QuestionForm을 조금 수정하면 어느정도 해결이 가능widgets속성을 지정하면subject,content입력 필드에form-control과 같은 부트스트랩 클래스를 추가 가능
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}

form labelSubject, Content를 영문이 아니라 한글로 표시하고 싶다면 다음처럼 labels 속성을 지정📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
labels = { # 추가
'subject': '제목', # 추가
'content': '내용', # 추가
} # 추가

수동 폼 작성{{ form.as_p }}
{{ form.as_p }}를 사용하면 빠르게 템플릿을 만들 수 있지만HTML코드가 자동으로 생성되므로 디자인 측면에서 많은 제한이 생김.- 예를 들어 폼 엘리먼트 내에 특정 태그를 추가하거나 필요한 클래스를 추가하는 작업에 제한이 생김.
- 또 디자인 영역과 서버 프로그램 영역이 혼재되어 웹 디자이너와 개발자의 역할을 분리하기도 모호해짐.
이용하여 자동으로 HTML 코드를 생성하지 않고 직접 HTML 코드를 작성하는 방법을 사용하기 위해 수작업시 필요없는 widget 속성을 제거
📄 /pybo/forms.py
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
{{ form.as_p }}로 자동으로 생성되는HTML대신 제목과 내용에 해당되는 HTML코드를 직접 작성- 그리고
question_create함수에서form.is_valid()가 실패할 경우 발생하는 오류의 내용을 표시하기 위해오류를 표시하는 영역을 추가
- 제목(
subject) 항목의value에는{{ form.subject.value|default_if_none:'' }}처럼 값을 대입해 주었는데 이것은 오류가 발생했을 경우 기존에 입력했던 값을 유지하기 위함.|default_if_none:''의 의미 : 폼 데이터(form.subject.value)에 값이 없을 경우None이라는 문자열이 표시되는데None대신공백으로 표시하라는 의미의 템플릿 필터
- 장고의 템플릿 필터는
|default_if_none:''처럼|기호와 함께 사용됨.
📄 /templates/pybo/question_form.html
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
<!-- 오류표시 Start --> # 추가 시작
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End -->
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" class="form-control" name="subject" id="subject"
value="{{ form.subject.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea class="form-control" name="content"
id="content" rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
</div> # 추가 끝
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
- "내용"에 아무런 값도 입력하지 않았기 때문에 "내용"을 입력하라는 오류메시지 확인 가능
- 그리고 "제목"에 입력했던 "TEST"는 사라지지 않고 계속 유지되는 것도 확인 가능.

답변 등록AnswerFormAnswerForm을 pybo/forms.py 파일에 다음과 같이 작성📄 /pybo/forms.py
from django import forms
from pybo.models import Question, Answer
(... 생략 ...)
class AnswerForm(forms.ModelForm):
class Meta:
model = Answer
fields = ['content']
labels = {
'content': '답변내용',
}
answer_create 함수answer_create 함수를 다음과 같이 수정
question_create와 같은 방법으로AnswerForm을 이용하도록 변경.- 하지만 답변 등록은
POST방식만 사용되기 때문에GET방식으로 요청할 경우에는HttpResponseNotAllowed오류가 발생하도록 설정.
📄 /pybo/views.py
(... 생략 ...)
from django.http import HttpResponseNotAllowed
from .forms import QuestionForm, AnswerForm
(... 생략 ...)
def answer_create(request, question_id):
"""
pybo 답변등록
"""
question = get_object_or_404(Question, pk=question_id)
if request.method == "POST":
form = AnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.create_date = timezone.now()
answer.question = question
answer.save()
return redirect('pybo:detail', question_id=question.id)
else:
return HttpResponseNotAllowed('Only POST is possible.')
context = {'question': question, 'form': form}
return render(request, 'pybo/question_detail.html', context)
question_detail.html📄 /templates/pybo/question_detail.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
(... 생략 ...)
<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
{% csrf_token %}
<!-- 오류표시 Start --> # 추가 시작
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End --> # 추가 끝
<div class="form-group">
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
</form>
</div>
{% endblock %}
