[Do it 장고 + 부트스트랩] 12장. 템플릿 모듈화하기

정재욱·2023년 6월 12일
0
post-thumbnail
post-custom-banner

대부분의 웹 사이트들은 웹 사이트 내의 다른 페이지를 열어도 일관된 페이지 디자인을 유지한다. 어떤 페이지를 열어도 내비게이션 바와 푸터는 그대로 유지되어있다. 이렇게 페이지에서 반복적으로 사용되는 요소는 페이지마다 동일한 소스 코드를 중복해서 사용할 필요 없이 모듈화하여 관리하면 판리하다.

12장에서는 내비게이션 바와 푸터처럼 웹 사이트 전반에 걸쳐 유지되어야 하는 요소를 모듈화하는 방법에 대해 알아본다.

메인 영역 모듈화 하기

다음 그림들은 차례대로 포스트 목록 페이지, 포스트 상세 페이지의 모습이다.

공통적으로 맨 위에 내비게이션 바가 있고, 그 아래 왼쪽에 메인 영역, 오른쪽에 사이드 영역, 아래에 푸터가 있다. 여기서 메인영역을 제외하고 나머지 영역은 디자인이 일관되게 유지시킬 것 이다.

즉, 메인 영역을 제외한 나머지 영역을 하나의 페이지에 만들어 놓고, 포스트 목록 페이지일 때와 포스트 상세 페이지일 때를 구분하여 메인 영역만 따로 채워줄 것 이다.

post_list.html 모듈화하기

base.html 만들기

먼저 blog/templates/blog/에 base.html을 만들고, post_list.html을 복사한 다음 붙여넣자. base.html은 공통 영역만을 남기기 위한 파일이다.

이후 base.html에서 id='main-area'div 요소의 내부(즉, 메인영역)를 삭제하자.
그리고 다음과 같이 {% block main_area %}{% endblock %}을 추가하여 main_area라는 이름의 블록을 만든다.

main_area 블록에 포스트 목록 페이지와 포스트 상세 페이지의 메인 영역 내용을 끼워넣을 것이다.

(..생략..)
<div class="container my-3">
  <div class="row">
    <div class="col-md-8 col-lg-9" id="main-area">
      <!-- <h1>Blog</h1> 삭제 -->
      (..생략..)
      <!-- </ul> 여기까지 삭제 -->
      {% block main_area %}
      {% endblock %}
    </div>
   
    <div class="col-md-4 col-lg-3">

base.html을 확장하여 post_list.html 넣기

이제 post_list.html에는 block 안에 들어가는 요소만 있으면 되기 때문에 base.html에서 지웠던 부분만 남기고 나머지를 전부 지운다.

그리고 맨 위에 {% extends 'blog/base.html' %}를 추가하고, 남긴 요소 앞 뒤로 {% block main_area %}{% endblock %}으로 블록의 시작과 끝을 지정한다.

그럼 앞에서 만든 base.html의 main_area 블록을 post_list.html의 블록에 들어있는 내용으로 채워진다.

<!-- post_list.html -->
{% extends 'blog/base.html' %}

{% block main_area %}
    <h1>Blog</h1>
    {% if post_list.exists %}

        {% for p in post_list %}
            <!-- Blog post-->
            <div class="card mb-4">
                {% if p.head_image %}
                    <img class="card-img-top" src="{{ p.head_image.url }}" alt="{{ p }} head image" />
                {% else %}
                    <img class="card-img-top" src="http://picsum.photos/seed/{{ p.pk }}/800/200" alt="random image">
                {% endif %}
                <div class="card-body">
                    <h2 class="card-title h4">{{ p.title }}</h2>
                    {% if p.hook_text %}
                        <h5 class="text-muted">{{ p.hook_text }}</h5>
                    {% endif %}
                    <p class="card-text">{{ p.content | truncatewords:45 }}</p>
                    <a class="btn btn-primary" href="{{ p.get_absolute_url }}">Read more →</a>
                </div>
                <div class="card-footer small text-muted">Posted on {{ p.created_at }} by <a href="#">작성자명 쓸 위치(개발예정)</a>
                </div>
            </div>
        {% endfor %}

    {% else %}
        <h3>아직 게시물이 없습니다.</h3>
    {% endif %}

    <!-- Pagination-->
    <nav aria-label="Pagination">
        <hr class="my-0" />
        <ul class="pagination justify-content-center my-4">
            <li class="page-item disabled"><a class="page-link" href="#" tabindex="-1"
                    aria-disabled="true">Newer</a></li>
            <li class="page-item active" aria-current="page"><a class="page-link" href="#!">1</a></li>
            <li class="page-item"><a class="page-link" href="#!">2</a></li>
            <li class="page-item"><a class="page-link" href="#!">3</a></li>
            <li class="page-item disabled"><a class="page-link" href="#!">...</a></li>
            <li class="page-item"><a class="page-link" href="#!">15</a></li>
            <li class="page-item"><a class="page-link" href="#!">Older</a></li>
        </ul>
    </nav>
{% endblock %}

수정 결과 확인하기

이제 테스트코드를 돌려보면 test_post_list() 함수는 통과하고, test_post_detail() 함수에서 Fail이 나오는 것을 볼 수 있다.

만약 test_post_liet() 함수만 테스트 하고 있다면, python manage.py test blog.tests.TestView.test_post_list를 하면 된다.
이는 python manage.py test 앱.(테스트 파일 이름).클래스.함수 순이다.

post_detail.html 모듈화 하기

이제 포스트 상세 페이지를 모듈화 해보자

base.html을 확장하여 post_detail.html에 넣기

방법은 앞서 post_list.html을 모듈화한 방법과 동일하다.

post_detail.html에서 메인 영역에 해당하는 부분만 남기고 다 지운 후 블록을 지정하면 된다.

{% extends 'blog/base.html' %}

{% block main_area %}

  <!-- Title -->
  <h1 class="mt-4">{{ post.title }}</h1>
  {% if post.hook_text %}
  <h5 class="text-muted">{{ post.hook_text }}</h5>
  {% endif %}

  <!-- Author -->
  <p class="lead">
    by
    <a href="#">작성자명 쓸 위치(개발예정)</a>
  </p>

  <hr>

  <!-- Date/Time -->
  <p>Posted on {{ post.created_at }}</p>

  <hr>

  <!-- Preview Image -->
  (..생략..)
  <hr>

  <!-- Post Content -->
  (..생략..)
  <hr>

  <!-- Comments Form -->
  <div class="card my-4">
    (..생략..)
  </div>

  <!-- Single Comment -->
  <div class="media mb-4">
    (..생략..)
  </div>

  <!-- Comment with nested comments -->
  <div class="media mb-4">
    (..생략..)
  </div>

{% endblock %} <!-- 추가 -->

이제 테스트코드를 돌려보자.

그려면 위와 같이 Fail이 발생한다. 자세히 보니 웹 브라우저 위쪽에 나타나는 타이틀(첫 번째 포스트의 제목)이 없다는 에러를 알려주고 있다.

제목 블록을 따로 만들기

웹 브라우저의 타이틀은 <head> 태그 안에 있고, <head> 태그는 base.html에 있다.

앞서 우리는 base.html을 post_list.html을 복붙하여 만들었기 때문에 <head>태그를 살펴보면 다음과 같이 되어있는 것을 볼 수 있다.

<!DOCTYPE html>
{% load static %}
<html>

<head>
    <title>Blog</title>
  	(..생략..)
</head>

필자는 포스트 리스트 목록에서는 웹 브라우저의 타이틀이 'Blog'라고 나오게끔 하고, 포스트 상세 목록에서는 '포스트 제목 - Blog'가 나오게끔 하려고 한다.

우선 post_detail.html에 다음과 같이 head_title이라는 블록을 추가하자.

{% extends 'blog/base.html' %}

{% block head_title %}
    {{ post.title }} - Blog
{% endblock %}

{% block main_area %}
    <div id="post-area">
        <!-- Title -->
(..생략..)

이제 base.html의 <title> 태그 안에도 블록을 지정한다.

<!DOCTYPE html>
{% load static %}
<html>

<head>
    <title>{% block head_title %}Blog{% endblock %}</title>
  	(..생략..)
</head>

이렇게 하면 base.html을 extends한 다른 템플릿 파일에 head_title 블록이 있을 경우에는, 그 내용을 base.html의 head_title 블록에 채워 넣게 된다.

하지만, post_list.html과 같이 head_title 블록이 명시되어 있지 않은 경우에는, 기본값인 'Blog'를 사용하는 구조다.

서버를 실행시키고, 웹 브라우저을 열어 확인해보면 다음과 같이 내비게이션 바가 동일하게 적용된 것과, 메인영역과 사이드영역의 비율이 동일하게 적용된 것을 볼 수 있다.

내비게이션바 고정

지난번에 9장? 에서 post_detail.html에서 내비게이션바를 고정하는 기능을 구현했었다.

하지만 이번에 모듈화를 하면서 base.html을 post_list.html의 내용을 기반으로 작성했기 때문에 해당 기능이 사라져 버렸다.

내비게이션바를 고정하기 위해서는 다음과 같이 <nav> 태그에 fixed-top을 추가한다.

이후 전에 구현했던 blog-post.css 안에 패딩을 주는 설정이 있으므로 해당 css파일을 사용하기 위해 base.html을 약간의 수정을 해준다.

이후 확인해보면 내비게이션 바가 고정되어 있는 것을 볼 수 있다.


내비게이션 바와 푸터 모듈화 하기

내비게이션 바 버튼에 링크 추가하기

내비게이션 바와 푸터를 모듈화 하기 전에, 내비게이션 바 버튼에 링크를 추가해보자.
현재까지 내비게이션 바에 있는 버튼(Blog, About Me 등)에는 링크가 달려있지 않아, 해당 버튼을 클릭해도 페이지 이동이 되지 않았다.

base.html을 열어 다음과 같이 수정하자.

테스트 코드 작성하기

내비게이션 바의 정상 유무를 확인하는 테스트 코드는 10장에서 작성했었다.
test_post_list()함수와 test_post_detail()함수에 동일하게 들어 있다.

해당 코드를 따로 떼어내어 함수로 따로 만들어보자.

from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post


class TestView(TestCase):
    def setUp(self):
        self.client = Client()

    def navbar_test(self, soup):
        navbar = soup.nav
        self.assertIn('Blog', navbar.text)
        self.assertIn('About Me', navbar.text)
        
        logo_btn = navbar.find('a', text='Do It Django')
        self.assertEqual(logo_btn.attrs['href'], '/')

        home_btn = navbar.find('a', text='Home')
        self.assertEqual(home_btn.attrs['href'], '/')

        blog_btn = navbar.find('a', text='Blog')
        self.assertEqual(blog_btn.attrs['href'], '/blog/')

        about_me_btn = navbar.find('a', text='About Me')
        self.assertEqual(about_me_btn.attrs['href'], '/about_me/')
        
    def test_post_list(self):
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)
        soup = BeautifulSoup(response.content, 'html.parser')
        self.assertEqual(soup.title.text, 'Blog')

        self.navbar_test(soup) # 수정
        (..생략..)
    
    def test_post_detail(self):
        post_001 = Post.objects.create(
            title = '첫 번째 포스트입니다.',
            content='Hello World'
        )
        self.assertEqual(post_001.get_absolute_url(), '/blog/1/')


        response = self.client.get(post_001.get_absolute_url())
        self.assertEqual(response.status_code, 200)
        soup = BeautifulSoup(response.content, 'html.parser')

        self.navbar_test(soup) # 수정
        (..생략..)

내비게이션 바를 점검하는 함수명은 navbar_test()로 하였다. 왜냐하면 TestCase를 사용했을 때, 내부 함수 이름이 test로 시작하면 테스트를 위한 함수라고 인식하기 때문이다. 필자는 navbar_test()함수를 각 테스트 함수인 test_post_list()test_post_detail()에 넣을것이기 때문에 navbar_test()로 하였다.

navbar_test()는 각 테스트 함수에서 beautifulsoup을 통해 가져온 파싱된 HTML 요소를 인자로 받는다.

navbar_test()의 역할은 내비게이션 바에 있는 버튼을 클릭했을 때 제대로 된 링크로 연결되는지 확인하는 것이다. 예를 들어 'Do It Django' 또는 'Home' 버튼을 클리갛면 도메인 뒤에 아무것도 붙어 있지 않은 URL로 이동해야 한다.

logo_btn = navbar.find('a', text='Do It Django') 을 통해 navbar에서 <a>태그 안에 'Do It Django'라는 텍스트를 찾아 logo_btn에 저장한다.

self.assertEqual(logo_btn.attrs['href'], '/')에서 logo_btn.attrs['href']는 logo_btn 요소의 href 속성 값을 가져온다. 이것은 HTML 요소에서 href 속성의 값을 확인하기 위해 사용된다. 즉, logo_btn 요소의href 속성 값이 /와 동일한지 확인하는 것이다.

이제 테스트를 해보면 드디어 OK가 나온다.
또 실제로 잘 되었는지 확인하기 위해 서버를 실행시켜 웹 브라우저로도 확인해보자.


include로 내비게이션 바와 푸터 모듈화하기

  • include : 템플릿 파일에서 다른 템플릿 파일을 포함시키는 데 사용된다. 즉, 템플릿에서 재사용 가능한 부분을 분리하여 다른 템플릿 파일로 만들고, 이를 include 지시문을 사용하여 원래 템플릿에 포함시킬 수 있다. include는 템플릿의 특정 부분을 모듈화하고 재사용 가능하게 만들어주는 유용한 기능이다.
    예를 들어, 이후 만들 navbar.html 템플릿 파일에는 웹 사이트의 내비게이션 바 기능이 들어있다. 이 파일을 다른 템플릿에서 재사용하고 싶다면, 해당 템플릿에 include 'navbar.html'와 같은 형식으로 포함시킬 수 있다.
  • extends : 템플릿 상속을 구현하는 데 사용된다. 템플릿 상속은 기본 템플릿을 만들고, 이를 확장하여 추가 블록을 포함한 특정 템플릿을 생성하는 기능이다. 상속을 통해 공통된 레이아웃이나 기능을 가진 기본 템플릿을 정의하고, 이를 확장하여 각각의 특정 페이지에 맞는 내용을 추가할 수 있다.
    예를 들어, 기본 템플릿을 base.html로 정의하고, 이를 확장하여 child.html을 생성하는 경우, child.html 파일의 맨 위에 {% extends 'base.html' %}라는 선언을 추가한다. 이렇게 하면 child.html에서는 base.html의 내용을 상속받고, 필요에 따라 추가 블록을 재정의하여 원하는 내용을 채울 수 있다.

간단히 말하면, include는 다른 템플릿 파일을 현재 템플릿에 포함시키는 데 사용되고, extends는 기본 템플릿을 확장하여 특정 템플릿을 만드는 데 사용된다.

내비게이션 바를 navbar.html로 모듈화 하기

blog/templates/blog/ 폴더에 navbar.html을 만든다. 그리고 base.html의 <nav> 태그부터 그 아래 모달에 관련된 코드까지 잘라 navbar.html에 붙어 넣는다.

<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
    <div class="container">
        <a class="navbar-brand" href="/">Do It Django</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown"
            aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        
      (..생략..)
      
    </div>
</nav>

<!-- LogIn Modal -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="logInModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        
      (..생략..)
      
    </div>
</div>

이후 base.html에서 방금 자르고 나서 비어버린 곳은 {% include 'blog/navbar.html' %}로 채워 넣는다. 그럼 나중에 base.html을 사용할 때 이 위치에 navbar.html의 내용을 그대로 가져와서 붙여 넣는 것과 동일한 효과를 얻을 수 있다.

<!DOCTYPE html>
{% load static %}
<html>

<head>
    <title>{% block head_title %}Blog{% endblock %}</title>
    <link href="{% static 'blog/bootstrap/bootstrap.min.css' %}" rel="stylesheet" media="screen">
    <link href="{% static 'blog/css/blog-post.css' %}" rel="stylesheet" media="screen">
    <script src="https://kit.fontawesome.com/0af178cae1.js" crossorigin="anonymous"></script>
</head>

<body>
    {% include 'blog/navbar.html' %}
  (..생략..)

푸터를 footer.html로 모듈화 하기

마찬가지로 푸터도 base.html에서 <footer> 태그 내용을 잘라내고 새로운 footer.html을 만든 다음 붙여넣기 하자.


정리

  • 모듈화

  • includeextends의 차이 알고가기

  • 테스트 코드 수정

profile
AI 서비스 엔지니어를 목표로 공부하고 있습니다.
post-custom-banner

0개의 댓글