Computer Science/BackEnd

점프 투 장고 #3-1 | 파이보 서비스 개발 - 내비게이션 바, 페이징, 템플릿 필터, 답변 개수 표시, 로그인과 로그아웃

토마토. 2022. 8. 18. 15:53

[참고] 3장 파이보 서비스 개발! - 점프 투 장고 (wikidocs.net)

 

3장 파이보 서비스 개발!

파이보의 기초 공사가 마무리되었으니 이제 본격적으로 파이보를 만들 차례이다. 이 장을 마치면 파이보는 꽤 괜찮은 모습으로 거듭날 것이다. 파이보를 조금씩 발전시켜 나가는 즐 ...

wikidocs.net

 

점프 투 장고 3장에서는 파이보 서비스를 본격적으로 개발한다. 

게시판 CRUD 기능, 로그인/로그아웃, 게시판 페이징, 회원가입, 검색 기능을 추가한다.

그리고 부트스트랩을 적용하여 웹 페이지의 심미성을 높인다. 

일단 이 글에서는 네비게이션 바, 페이징, 템플릿 필터, 답변 개수 표시, 로그인/로그아웃을 먼저 살펴보자. 

기능을 추가하고 화면을 바꾸는 걸 반복하면서 위 사진에서 보이는 장고 아키텍쳐에 대한 감을 잡게 된다. 

클라이언트의 요청을 받으면, urls.py에서 views.py로 넘겨주고, views.py에서 template을 불러와 화면을 보여준다.

 

 

내비게이션 바

파이보 메인 페이지에 햄버거 모양의 네비게이션 바를 만들고자 한다. 

네비게이션 바는 장고 부트스트랩 컴포넌트로 제공되는 것이다. 

모든 화면에서 보여주어야 하는 기능이므로 base.html 템플릿에 추가해줄 것이다. 

 

</templates/base.html> 

{% load static %}
<!doctype html>
<html lang="ko">
    <head>
        <!--메타 태그-->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!--부트스트랩 CSS-->
        <link rel="stylesheet" type="text/css" href="{%static 'bootstrap.min.css'%}">
        <!--파이보 CSS-->
        <link rel="stylesheet" type="text/css" href="{%static 'style.css'%}">
        <title>Hello, pybo!</title>
    </head>
    <body>
    
    
        <!--내비게이션 바-->
        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <div class="container-fluid">
                <a class="navbar-brand" href="{%url 'pybo:index'%}">Pybo</a>
                <button class="navbar-toggler" type="button"
                    data-bs-toggle="collapse"
                    data-bs-target="#navbarSupportedContent"
                    aria-controls="navbarSupportedContent"
                    aria-expanded="false"
                    aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link" href="#">로그인</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    
    
        <!--기본 템플릿을 상속받아 삽입될 내용-->
        {%block content%}
        {%endblock%}
    </body>
</html>

이렇게 <nav> 태그 시작부터 </nav> 태그 끝까지 네비게이션에 관한 내용이다.

네비게이션 바에 햄버거 버튼과 함께 로그인 링크를 추가해주었다. 

 

이렇게 햄버거 버튼이 생겼으나 버튼을 누른다고 해서 반응하지는 않는다. 

반응형 웹이기 때문에 웹 브라우저의 크기를 줄이면 어느 순간 네비게이션 메뉴 버튼이 생기고, 로그인 링크는 사라진다. 

 

이렇게 반응형 웹으로 웹이 반응하게 하려면, 부트스트랩 자바스크립트 파일인 bootstrap.min.js를 포함시켜야 한다. 

 

        <!--기본 템플릿을 상속받아 삽입될 내용-->
        {%block content%}
        {%endblock%}
        <!--부트스트랩-->
        <script src="{%static 'bootstrap.min.js' %}"></script>
    </body>

static 폴더에 bootstrap.min.js를 복사한 뒤에,

base.html의 맨 아래에 bootstrap.min.js를 추가해주었더니 변화가 생긴 걸 확인할 수 있다. 

 

짠! 로그인 링크가 생겼다. 

 

  • include 태그

include 태그는 다른 템플릿 파일을 포함해서 재사용할 수 있게 해주는 기능이다. 

특정 영역이 반복적으로 등장할 때 코드의 중복을 없애기 위해 사용한다. 

만약에 재사용하고자 하는 부분을 abc.html로 분리했을 때, 

다른 파일에서 넣으려면, {%include "abc.html" %}만 넣어주면 된다. 

 

우리는 네비게이션 바에 필요한 내용을 navbar.html 파일에 분리해두고자 한다. 

 

<navbar.html>

<!--네비게이션 바에만 필요한 내용 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a class="navbar-brand" href="{%url 'pybo:index'%}">Pybo</a>
        <button class="navbar-toggler" type="button"
            data-bs-toggle="collapse"
            data-bs-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent"
            aria-expanded="false"
            aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
                <a class="nav-link" href="#">로그인</a>
            </li>
        </ul>
    </div>
</div>
</nav>

navbar.html 템플릿을 생성해주고, 

이 템플릿을 base.html에 {%include "navbar.html"%}로 추가해준다. 

 

 

<base.html> 중

        <!--내비게이션 바-->
        {% include "navber.html" %}

 

페이징

이제 pybo 게시판에 페이징 기능을 추가하고자 한다. 

페이징 기능이라는 건, 모든 게시물을 한 화면에 보여주는 게 아니라 10개, 20개씩 끊어서 한 페이지에 보여주는 기능을 말한다. 

 

페이지 기능을 테스트하기 위해 일단 대량 데이터를 생성해보자. 

이를 위해 장고 셸이 필요하다. 

 

 python manage.py shell
Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

이렇게 셸을 실행하고, 

 

>>> from pybo.models import Question
>>> from django.utils import timezone
>>> for i in range(300):
...     q = Question(subject='테스트데이터입니다:[%03d]' %i, content='내용없음', 
create_date=timezone.now())
...     q.save()

테스트 데이터를 생성해준다. 

 

페이징 기능을 해주는 건 django.core의 Paginator 클래스다. 

views.py 함수에서 paginator 클래스를 이용해 페이징을 해주자. 

from django.core.paginator import Paginator
# Create your views here.


def index(request):
    page = request.GET.get('page','1') # 페이지
    question_list = Question.objects.order_by('-create_date')
    paginator = Paginator(question_list, 10) # 페이지당 10개씩 보여주기
    page_obj = paginator.get_page(page)
    context = {'question_list' : page_obj}
    return render(request, 'pybo/question_list.html', context)

하나씩 보면, 

page = request.GET.get('page', '1')은 GET 방식으로 호출된 URL에서 page 값을 가져올 때 사용한다. 

page 값 없이 호출될 때는 디폴트 1이다. 

 

paginator 클래스를 (quesiton_list, 10)으로 사용한다. 

question_list는 게시물 전체 데이터, 두번째 파라미터 10은 페이지당 보여줄 글의 수다. 

 

page_obj = paginator.get_page(page)

에서는 page에 해당하는 페이징 객체 page_obj를 생성해준 것이다. 

 

 

짠! 페이지가 생겼다. 

템플릿 필터

템플릿 필터란 | 문자 뒤에 사용하는 필터를 의미한다. 

 

from django import template

register = template.Library()

@register.filter
def sub(value, arg):
    return value - arg

이렇게 만든 템플릿 필터를 이용해서, 

게시판 글의 번호를 정확하게 보여줄 수 있다. 

 

 

답변 개수 표시
                <td>
                    <a href="{%url 'pybo:detail' question.id %}">{{question.subject}}</a>
                    {%if question.answer_set.count > 0%}
                    <span class="text-danger small mx-2">{{question.answer_set.count}}</span>
                    {%endif%}
                </td>

 

로그인과 로그아웃

장고의 django.contrib.auth를 이용해서 로그인, 로그아웃을 구현할 수 있다. 

 

로그인, 로그아웃 기능은 pybo가 아니라 common 앱에서 구현해준다. 

회원가입과 로그인/로그아웃 기능은 다른 프로젝트에서도 필요하기 때문이다. 

 

common 앱을 시작해준다. 

$ django-admin startapp common

 

common 앱이 생긴걸 볼 수 있다. 

 

이제 pybo를 만들 때 했던 것처럼 settings.py에 common 앱을 등록하자. 

 

<settings.py>

INSTALLED_APPS = [
    'common.apps.CommonConfig',
    'pybo.apps.PyboConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

 

그 다음에는 common의 url을 사용하기 위해 config/urls.py에 common의 urls.py 파일을 등록해준다. 

urlpatterns = [
    path('admin/', admin.site.urls),
    path('pybo/', include('pybo.urls')),
    path('common/', include('common.urls')),
]

 

<common/urls.py>

app_name = 'common'

urlpatterns = [
    
    
]

 

 

<navbar.html>에서

                <a class="nav-link" href="{% url 'common:login' %}">로그인</a>

이렇게 로그인 링크를 common:login으로 바꾸어준다. 

 

로그인 뷰

from django.urls import path
from django.contrib.auth import views as auth_views

app_name = 'common'

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(), name='login')
    
]

URL 매핑 규칙 common/urls.py는 login 링크를 받으면, django.contrib.auth 앱이 가지고 있는 auth_views.LoginView를 보여준다. 

 

로그인 템플릿

{%extends "base.html"%}
{%block content%}
<div class="container my-3">
    <form method="post" action="{% url 'common:lobin'%}">
        {%csrf_token%}
        {%include "form_errors.html" %}
        <div class="mb-3">
            <label for="username">사용자ID</label>
            <input type="text" class="form-control" name="username" id="username"
                value="{{form.username.value|default_if_none:''}}">
        </div>
        <div class="mb-3">
            <label for="password">비밀번호</label>
            <input type="password" class="form-control" name="password" id="password"
                value="{{form.password.value|default_if_none:''}}">
        </div>
        <button type="submit" class="btn btn-primary">로그인</button>
    </form>
</div>

{%endblock%}

이 페이지에서 받는 username과 password 항목은 필수 항목이다.

 

<!-- 필드 오류, 넌필드 오류를 출력 -->
{% if form.errors %}
<div class="alert alert-danger">
    {% for field in form %}
    <!-- 필드 오류 -->
    {% if field.errors %}
    <div>
        <strong>{{field.label}}</strong>
        {{field.errors}}
    </div>
    {%endif%}
    {%endfor%}
    <!--넌필드 오류 -->
    {%for error in form.non_field_errors %}
    <div>
        <strong>{{error}}</strong>
    </div>
    {%endfor%}
</div>
{%endif%}

form_errors.html 태그 파일을 생성해준다.

form_errors에서 필드 에러랑 논필드 에러를 표시해준다고 했다. 필드 에러는 필드 형식이 맞지 않거나 누락되었거나 해서 발생하는 에러다. 넌필드 에러는 그것과 상관없이 발생하는 에러다. 

 

이렇게 화면이 생성되었다. 

 

#로그인 후에 이동하는 URL
LOGIN_REDIRECT_URL='/'

settings.py에서 설정해준다. 

 

이제 이것에 맞게 URL 매핑 규칙을 추가해주자. 

<urls.py>

urlpatterns = [
    path('admin/', admin.site.urls),
    path('pybo/', include('pybo.urls')),
    path('common/', include('common.urls')),
    path('', views.index, name='index'),
]

로그아웃

로그인된 상태에서는 로그인 버튼을 로그아웃 버튼으로 바꾸어주어야 한다. 

    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
                {%if user.is_authenticated %}
                <a class="nav-link" href="{%url 'common:logout'%}">{{user.username}}(로그아웃)</a>
                {%else%}
                <a class="nav-link" href="{% url 'common:login' %}">로그인</a>
                {%endif%}
            </li>
        </ul>
    </div>
</div>
</nav>

navbar.html 템플릿에서 if문을 추가해준다. 

user.is_authenticated으로 유저가 로그인된 상태를 확인한다면, 

로그아웃 링크를 표시하고 이를 common:logout과 연결하는 것이다. 

 

이제 이것에 맞게 common:logout을 url부터 추가해주어야 한다. 

from django.urls import path
from django.contrib.auth import views as auth_views

app_name = 'common'

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login')
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]

매핑을 추가해주었다.