답변을 작성하거나 수정하면 질문 상세 화면의 브라우저 스크롤바가 항상 페이지 상단으로 고정됨
앵커 엘리먼트로 스크롤 문제 해결 가능
aname 과 URL 뒤 # 뒤 문자열이 일치하면 해당 앵커 요소 위치로 스크롤이 이동질문 상세 화면에 앵커 요소 추가
templates\pybo\question_detail.html
<!-- 딥변 내용 -->
{% for answer in question.answer_set.all %}
<a name="answer_{{answer.id}}"></a>
앵커 요소로 이동할 수 있도록 redirect 수정
pybo\views\answer_views.py
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
...
@login_required(login_url='common:login')
def answer_create(request, question_id):
...
return redirect('{}#answer_{}'.format(resolve_url('pybo:detail', question_id=question.id), answer.id))
...
@login_required(login_url='common:login')
def answer_modify(request, answer_id):
...
return redirect('{}#answer_{}'.format(resolve_url('pybo:detail', question_id=answer.question.id), answer.id))
...
format 과 resolve_url 함수를 사용resolve_url : 실제 호출되는 URL을 문자열로 반환하는 함수위 과정과 동일
파이보에 마크다운을 적용하기 전에 마크다운의 문법을 간단하게 알아보자.
목록을 표시하기 위해 다음처럼 작성한다.
* 파이썬
* 장고
* 알고리즘
위 문자열을 마크다운 해석기가 HTML로 변환하면 실제 화면에서는 다음과 같이 보인다.
마크다운 해석기는 조금 후에 설치하고 실습할 것이니 우선은 마크다운 문법에 집중하자.
순서가 있는 목록을 표시하려면 다음처럼 작성한다.
1. 하나
1. 둘
1. 셋
결과는 다음과 같다.
작성한 글자를 강조 표시하려면 강조할 텍스트 양쪽에 **를 넣어 감싼다.
장고는 **파이썬**으로 만들어진 웹 프레임워크이다.
결과는 다음과 같다.
장고는 파이썬으로 만들어진 웹 프레임워크이다.
HTML 링크는 다음처럼 [링크명](링크주소) 규칙을 적용하여 생성한다.
파이썬 홈페이지는 http://www.python.org 입니다.
결과는 다음과 같다.
파이썬 홈페이지는 http://www.python.org 입니다.
소스코드는 백쿼트 ` 3개를 연이어 붙여 위아래로 감싸면 생성할 수 있다.
백쿼트는 백틱이라고도 한다.

결과는 다음과 같다.
def add(a, b):
return a+b
인용을 표시하려면 다음처럼 >를 문장 맨 앞에 입력하고 1칸 띄어쓰기를 한 다음 인용구를 입력한다.
> 마크다운은 Github에서 사용하는 글쓰기 도구이다.
결과는 다음과 같다.
마크다운은 Github에서 사용하는 글쓰기 도구이다.
마크다운의 보다 자세한 사용방법은 다음 문서를 참고하자.
마크다운 문법 공식 문서: www.markdownguide.org/getting-started
markdown 설치
pip install markdown
마크다운 필터 등록
pybo\templatetags\pybo_filter.py
import markdown
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter
def mark(value):
extensions = ['nl2br', 'fenced_code']
return mark_safe(markdown.markdown(value, extensions=extensions))
mark 함수는 markdown 모듈과 mark_safe 함수를 이용하여 문자열을 HTML 코드로 변환하여 반환함markdown : 일반 텍스트를 HTML로 변환하는 파이썬 패키지 : 마크다운 → html 태그로 변환template : Django 템플릿 시스템에서 커스텀 태그나 필터를 만들 때 필요mark_safe : 변환된 문자열을 “안전한 HTML”로 표시해, 템플릿에서 자동 이스케이프(escape)를 방지<h1> 등의 태그가 실제로 렌더링되도록 함<h1>이 문자 그대로 표시됨register = template.Library() : 필터 or 함수를 등록하기 위한 객체화template.Library()의 역할template.Library() 는 Django 템플릿 시스템에 새로운 필터나 태그를 등록할 수 있게 해주는 “등록소(Registry)” 객체를 생성합니다. 즉,“이 파일 안에 정의된 함수들을 Django 템플릿에서 쓸 수 있도록 등록하겠다”
라는 뜻이에요.
upper, lower, date 등) 외에도, 개발자가 직접 만든 커스텀 필터(custom filter) 나 커스텀 태그(custom tag) 를 쓸 수 있습니다. 하지만 Django는 “아무 함수나 자동으로 템플릿에서 쓰게 해주지 않아요”. 그래서 template.Library()를 통해 “이 함수는 내가 템플릿용으로 등록하겠다” 하고 알려줘야 합니다.template.Library() → 등록기 생성
@register.filter → 필터로 등록
@register.simple_tag → 태그로 등록
template.Library()는 “Django에게 이 파일 안의 함수를 템플릿 필터/태그로 쓸 거야” 라고 알려주는 등록기 객체
@register.filter : 실제 등록value : 마크다운 언어로 입력받을 값'nl2br' : 확장 도구 : 줄바꿈 문자를 Enter 를 한 번만 눌러도 줄바꿈으로 인식함'fenced_code' : 확장 도구 : 마크다운의 소스 코드 표현<!-- templates/blog/detail.html -->
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
여기서 {{ post.title }} 은 뷰(view) 함수에서 전달한 파이썬 객체의 값을 출력하는 부분입니다. 뷰에서 이렇게 연결합니다:def detail(request, post_id):
post = Post.objects.get(id=post_id)
return render(request, 'blog/detail.html', {'post': post})
→ Django는 post 데이터를 템플릿에 넣고, {{ post.title }} 을 실제 값으로 바꿔서 HTML로 만들어 브라우저에 보냅니다.| 구분 | 예시 | 설명 |
|---|---|---|
| 변수 (Variable) | {{ post.title }} | 뷰에서 전달된 context 데이터 출력 |
| 태그 (Tag) | {% for post in posts %} ... {% endfor %} | 반복, 조건문, include 등 로직 제어 |
{{ 변수|필터명 }} 형태로 변수의 출력 값을 가공하는 도구입니다. 즉, 데이터 변환을 담당하는 미니 함수라고 생각하면 됩니다.
| 예시 | 설명 | 출력 결과 |
|---|---|---|
| `{{ name | upper }}` | 모두 대문자로 변환 |
| `{{ price | add:1000 }}` | 1000 더함 |
| `{{ date | date:"Y-m-d" }}` | 날짜 포맷 변경 |
templatetags 폴더 안에 다음처럼 작성:from django import template
register = template.Library()
@register.filter
def sub(value, arg):
return value - arg
템플릿:{% load custom_filters %}
{{ 10|sub:3 }} <!-- 결과: 7 -->
{% ... %} 형태로, 조건문, 반복문, include, URL reverse 등 제어 로직을 수행합니다.
| 예시 | 설명 |
|---|---|
{% if user.is_authenticated %} … {% endif %} | 조건문 |
{% for item in items %} … {% endfor %} | 반복문 |
{% include "header.html" %} | 다른 템플릿 삽입 |
{% url 'post_detail' post.id %} | URL 이름을 실제 링크로 변환 |
from django import template
register = template.Library()
@register.simple_tag
def greeting(name):
return f"Hello, {name}!"
템플릿:{% load custom_tags %}
{% greeting "수민" %}
→ 결과: Hello, 수민!| 구분 | 역할 | 예시 | 결과 |
|---|---|---|---|
| 템플릿 (Template) | 데이터 + HTML 결합 | {{ post.title }} | 향수 추천 플랫폼 |
| 필터 (Filter) | 값 변환 | `{{ name | upper }}` |
| 태그 (Tag) | 로직 제어 | {% for post in posts %} | 반복 구조 처리 |
Django의 템플릿은 데이터를 시각적으로 표현하는 HTML 생성 도구이며,
필터는 데이터를 가공하고, 태그는 템플릿 내 로직 흐름을 제어하는 명령어입니다.
| 구성요소 | 역할 | 대응되는 MVC 구성요소 |
|---|---|---|
| Model | 데이터와 비즈니스 로직 담당 | Model |
| Template | 사용자에게 보여지는 화면(UI) 담당 | View |
| View | 요청을 받아 Model과 Template을 연결하는 중간 제어 로직 | Controller |
데이터 구조 정의 및 데이터베이스 조작을 담당합니다.
Django의 ORM(Object Relational Mapper)을 통해 Python 코드로 DB를 다룰 수 있습니다.
예시:
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
create_date = models.DateTimeField(auto_now_add=True)
→ SQL 없이 Python으로 DB 테이블 정의 가능
사용자에게 보여질 화면(HTML) 을 담당합니다.
Django Template Language(DTL)를 사용해 데이터를 표시합니다.
예시:
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
사용자의 요청을 받고, Model에서 데이터를 가져와, Template으로 전달하는 중간 제어 로직입니다.
Controller 역할을 합니다.
예시:
from django.shortcuts import render
from .models import Post
def post_detail(request, post_id):
post = Post.objects.get(id=post_id)
return render(request, 'blog/post_detail.html', {'post': post})
request → 사용자 요청 수신
Post.objects.get(...) → Model 사용
render(...) → Template에 데이터 전달
| 구분 | Django (MTV) | 전통 MVC |
|---|---|---|
| Model | Model | Model |
| Template | View (화면 표시) | View |
| View | Controller (요청 처리) | Controller |
| Controller | Django 프레임워크 내부에 내장됨 (URLconf + Dispatcher) | 명시적으로 구현 |
Django는 MTV(Model–Template–View) 구조를 따르며,
본질적으로 MVC 패턴을 Django 방식으로 재해석한 형태입니다.
즉, “역할은 같지만 이름이 다르다”고 이해하면 됩니다.
질문 상세 템플릿에 마크 다운 적용
templates\pybo\question_detail.html
{% extends 'base.html' %}
{% load pybo_filter %}
{% block content %}
...
<div class="card-text" style="white-space: pre-line;">
{{ question.content|mark }}
</div>
...
...
<div class="card-text">
{{ answer.content|mark }}
</div>
...
질문 목록 화면에 검색창 추가
templates\pybo\question_list.html
...
<div class="container my-3">
<div class="row justify-content-end my-3">
<div class="col-4 input-group">
<input type="text" class="form-control kw"
value="{{ kw|default_if_none:'' }}">
<div class="input-group-append">
<button class="btn btn-outline-secondary"
type="button" id="btn_search">찾기</button>
</div>
</div>
</div>
...
질문 목록 템플릿에 form 요소 추가
templates\pybo\question_list.html
...
<form id="searchForm" method="get" action="{% url 'index' %}">
<input type="hidden" id="kw" name="kw" value="{{ kw|default_if_none:'' }}">
<input type="hidden" id="page" name="page" value="{{ page }}">
</form>
{% endblock %}
질문 목록의 페이징 수정
templates\pybo\question_list.html
<a class="page-link" data-page="{{ question_list.previous_page_number }}" href="#">이전</a>
4개 추가
질문 목록 템플릿에 페이징과 검색을 위한 자바스크립트 코드 추가
templates\pybo\question_list.html
{% block script %}
<script type="text/javascript">
$(document).ready(function(){
$(".page-link").on('click', function() {
$("#page").val($(this).data("page"));
$("#searchForm").submit();
});
$("#btn_search").on('click', function() {
$("#kw").val($(".kw").val());
$("#page").val(1);
$("#searchForm").submit();
});
});
</script>
{% endblock %}
index 함수 수정
pybo\views\base_views.py
...
from django.db.models import Q
...
def index(request):
"""
pybo 목록 출력
"""
# 입력 인자
page = request.GET.get('page', '1') # 페이지
kw = request.GET.get('kw', '') # 검색어
# 조회
question_list = Question.objects.order_by("-create_date")
if kw:
question_list = question_list.filter(
Q(subject__icontains=kw) |
Q(content__icontains=kw) |
Q(author__username__icontains=kw) |
Q(answer__author__username__icontains=kw)
).distinct()
# 페이징 처리
paginator = Paginator(question_list, 10)
page_obj = paginator.get_page(page)
context = {"question_list" : page_obj, 'page': page, 'kw': kw}
return render(request, 'pybo/question_list.html', context)
정렬 기준
이러한 정렬 기준에 해당하는 파라미터 역시 GET 방식으로 요청해야 페이징, 검색, 정렬 기능이 잘 작동함
질문 목록 화면에 정렬 조건 추가
templates\pybo\question_list.html
...
<div class="row justify-content-between my-3">
<div class="col-2">
<select class="form-control so">
<option value="recent" {% if so == 'recent' %}selected{% endif %}>
최신순
</option>
<option value="recommend" {% if so == 'recommend' %}selected{% endif %}>
추천순
</option>
<option value="popular" {% if so == 'popular' %}selected{% endif %}>
인기순
</option>
</select>
</div>
...
질문 목록 템플릿의 searchForm 수정
templates\pybo\question_list.html
<form id="searchForm" method="get" action="{% url 'index' %}">
<input type="hidden" id="kw" name="kw" value="{{ kw|default_if_none:'' }}">
<input type="hidden" id="page" name="page" value="{{ page }}">
<input type="hidden" id="so" name="so" value="{{ so }}">
</form>
질문 목록 템플릿의 자바스크립트 코드 수정
templates\pybo\question_list.html
$(".so").on('change', function() {
$("#so").val($(this).val());
$("#page").val(1);
$("#searchForm").submit();
});
index 함수 수정
pybo\views\base_views.py
...
so = request.GET.get('so', 'recent') # 정렬 기준
# 정렬
if so == 'recommend':
question_list = Question.objects.annotate(
num_voter=Count('voter')).order_by('-num_voter', '-create_date')
elif so == 'popular':
question_list = Question.objects.annotate(
num_answer=Count('answer')).order_by('-num_answer', '-create_date')
else: # recent
question_list = Question.objects.order_by("-create_date")
# 조회
# question_list = Question.objects.order_by("-create_date")