Programming

; develop a program

Framework/Django

[Django] 장고(Django) 서비스 개발 - 페이징(Paging)

Clloud_ 2022. 12. 6. 23:56
반응형

이번 포스팅에서는 장고를 사용하여 게시판 서비스 개발에 필요한 페이징(Paging)에 대하여 공부를 해보고자 한다.

 


페이징(Paging)

페이징이란 고정 분할 방식으로 메모리를 분할하여 가상 주소를 물리 주소로 변환하는 방법이다.

단편화를 최소화하기 위해 메인 메모리와 가상 메모리를 같은 크기로 분할해 가지고 와서 할당한다.

 

가상 메모리는 하나의 분할된 영역을 page(페이지) - 논리 주소

물리 메모리는 하나의 분할된 영역을 Frame(프레임) - 물리 주소(실제의 주소)

 

페이지와 프레임의 크기는 같기 때문에 단편화(자투리 공간)를 줄여서 메모리를 효율적으로 사용할 수 있다.

즉, 고정 분할 방식(고정된 크기로)으로 메모리를 분할하여 가상 주소를 물리 주소로 변환하는 방법이다.

 

예를 들어 게시판 등과 같은 글의 목록 등을 구간 별로 나누어 페이지 형태로 보여주는 작업도 페이징이다.

게시판의 페이징 처리가 안되어 있으면 있는 모든 게시물이 한 페이지에 출력되어 스크롤바가 엄청 길어지는 현상이 나타난다.

 


대량 테스트 데이터 만들기

페이징을 구현하기 전에 페이징을 테스트할 수 있을 정도로 충분한 데이터를 생성한다.

대량의 테스트 데이터를 만드는 가장 좋은 방법은 장고 셸을 이용하는 것이다.

 

다음처럼 장고 셸을 실행한다.

(mysite) c:\projects\mysite>python manage.py shell
Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 22:45:29) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

 

이어서 질문 데이터를 생성하기 위한 모듈을 임포트 한다.

>>> from pybo.models import Question
>>> from django.utils import timezone

 

그리고 다음과 같이 300개의 테스트 데이터를 생성한다.

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

 

이제 장고 셸을 종료하고 로컬 서버를 실행한 다음 질문 목록을 조회하면 장고 셸로 등록한 테스트 데이터가 보인다.

 

 

그리고 300개 이상의 데이터가 한 페이지 전부 보이는 것을 확인할 수 있다.

이러한 이유로 페이징은 반드시 필요하다.

 


Paginator

장고에서 페이징을 위해 사용하는 클래스는 Paginator이다.

Paginator 클래스를 사용하여 index 함수에 페이징 기능을 적용한다.

 

[파일명: projects\mysite\pybo\views.py]

from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator  


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)

(... 생략 ...)

 

index 함수를 자세히 살펴보면 page = request.GET.get('page', '1')은 http://localhost:8000/pybo/?page=1 처럼 GET 방식으로 호출된 URL에서 page값을 가져올 때 사용한다.

만약 http://localhost:8000/pybo/ 처럼 page값 없이 호출된 경우에는 디폴트로 1이라는 값을 설정한다.

 

그리고 Paginator 클래스를 다음처럼 사용한다.

paginator = Paginator(question_list, 10) # 페이지당 10개씩 보여 주기

 

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

page_obj = paginator.get_page(page)

 

그리고 paginator를 이용하여 요청된 페이지(page)에 해당되는 페이징 객체(page_obj)를 생성한다.

이렇게 하면 장고 내부적으로는 데이터 전체를 조회하지 않고 해당 페이지의 데이터만 조회하도록 쿼리가 변경된다.

 

페이징 객체(page_obj) 속성

항목 설명
paginator.count 전체 게시물 개수
paginator.per_page 페이지당 보여줄 게시물 개수
paginator.page_range 페이지 범위
number 현재 페이지 번호
previous_page_number 이전 페이지 번호
next_page_number 다음 페이지 번호
has_previous 이전 페이지 유무
has_next 다음 페이지 유무
start_index 현재 페이지 시작 인덱스(1부터 시작)
end_index 현재 페이지의 끝 인덱스(1부터 시작)

 

이 속성들은 템플릿에서 페이징을 처리할 때 사용된다.

 


템플릿에 페이징 적용하기

index 함수에서 질문 목록 템플릿(pybo/question_list.html)에 전달한 데이터(context)는 다음과 같다.

context = {'question_list': page_obj}  # question_list는 페이징 객체(page_obj)
return render(request, 'pybo/question_list.html', context)

 

따라서 질문 목록 템플릿에 전달된 페이징 객체는 question_list이다.

페이징 객체인 question_list를 이용하여 템플릿에서 어떻게 페이징을 처리할 수 있는지 알아보기 위해 question_list.html 템플릿 파일의 </table> 태그 바로 밑에 다음 코드를 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_list.html]

(... 생략 ...)
    </table>
    <!-- 페이징처리 시작 -->
    <ul class="pagination justify-content-center">
        <!-- 이전페이지 -->
        {% if question_list.has_previous %}
        <li class="page-item">
            <a class="page-link" href="?page={{ question_list.previous_page_number }}">이전</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
        </li>
        {% endif %}
        <!-- 페이지리스트 -->
        {% for page_number in question_list.paginator.page_range %}
        {% if page_number == question_list.number %}
        <li class="page-item active" aria-current="page">
            <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
        </li>
        {% else %}
        <li class="page-item">
            <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
        </li>
        {% endif %}
        {% endfor %}
        <!-- 다음페이지 -->
        {% if question_list.has_next %}
        <li class="page-item">
            <a class="page-link" href="?page={{ question_list.next_page_number }}">다음</a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
        </li>
        {% endif %}
    </ul>
    <!-- 페이징처리 끝 -->
    <a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
{% endblock %}

 

상당히 많은 양의 HTML 코드가 추가되었다.

 

이전 페이지가 있는 경우에는 "이전" 링크가 활성화되게 하였고 이전 페이지가 없는 경우에는 "이전" 링크가 비활성화되도록 한다. (다음 페이지의 경우도 마찬가지 방법으로 적용된다.)

 

그리고 페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성한다.

이때 현재 페이지와 같을 경우에는 active클래스를 적용하여 강조표시를 한다.

 

위 템플릿에 사용된 주요 페이징 기능

페이징 기능 코드
이전 페이지가 있는지 체크 {% if question_list.has_previous %}
이전 페이지 번호 {{ question_list.previous_page_number }}
다음 페이지가 있는지 체크 {% if question_list.has_next %}
다음 페이지 번호 {{ question_list.next_page_number }}
페이지 리스트 루프 {% for page_number in question_list.paginator.page_range %}
현재 페이지와 같은지 체크 {% if page_number == question_list.number %}

 

그리고 페이지 리스트를 보기 좋게 표시하기 위해 부트스트랩의 pagination 컴포넌트를 이용하였다.

템플릿에 사용한 paginationpage-itempage-link 등이 부트스트랩 pagination 컴포넌트의 클래스이다.

 


페이지 리스트

 

페이징 처리는 잘 되었지만 한 가지 문제를 발견할 수 있다.

문제는 위에서 보듯이 이동할 수 있는 페이지가 모두 표시된다는 점이다.

 

이 문제를 해결하기 위해 다음과 같이 템플릿을 수정한다.

 

[파일명: projects\mysite\templates\pybo\question_list.html]

(... 생략 ...)
<!-- 페이지리스트 -->
{% for page_number in question_list.paginator.page_range %}
{% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
{% if page_number == question_list.number %}
<li class="page-item active" aria-current="page">
    <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% else %}
<li class="page-item">
    <a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
(... 생략 ...)

 

다음의 코드를 삽입하여 페이지 표시 제한 기능을 구현했다.

{% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
(... 생략 ...)
{% endif %}
여기서 사용한 |add:-5, |add:5 는 템플릿 필터이다. 
|add:-5는 5만큼 빼라는 의미이고 |add:5는 5만큼 더하라는 의미이다.

 

위 코드는 페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만든다.

현재 페이지를 의미하는 question_list.number보다 5만큼 크거나 작은 값만 표시되도록 만든 것이다. 

 

만약 현재 페이지가 15라면 다음처럼 페이지 리스트가 표시된다.

 

 

페이징은 사실 구현하기 까다로운 기술이다.

Paginator 클래스의 도움이 없었다면 아마 이렇게 쉽게 해내기는 힘들었을 것이다.

 

지금까지 만든 페이징 기능에 '처음'과 '마지막' 링크를 추가하고 '…' 생략 기호까지 추가하면 더 완성도 높은 페이징 기능이 될 것이다.

 


반응형