본문 바로가기
개발/웹

Flask 블로그 제작기 (4) - 페이지네이션 구현

by pandatta 2022. 9. 13.

플라스크로 서비스되던 구버전 블로그에서 옮겨왔습니다.


페이지네이션이란?

이 블로그, Dwarf in the Flask에는 페이지네이션(pagination) 기능이 있습니다. 이렇게 말하면 뭔가 싶겠지만, 사실 우리에게 익숙한 기능이죠, 바로 글 목록이 너무 길면 다음 페이지로 분리시켜주는 기능입니다. Bootstrap Pagination의 예시를 보면 더 와닿겠죠.

이 블로그는 Bootstrap도 사용하고있긴 하지만, 페이지네이션은 디자인적 측면보다는 구현 자체가 조금 어렵습니다. 총 25개의 포스트가 있고 한 페이지당 10개의 포스트를 보여주고싶다면, 25 / 10의 몫인 2만큼의 페이지를 만들고, 나머지인 5개의 포스트를 보여줄 페이지를 하나 더 만든 다음, 각 페이지에 대한 링크들과, 1페이지와 끝페이지에 대한 링크도 만들어줘야하죠. 물론 못할 일은 아니지만, 귀찮음을 재사용으로 극복하는 것이 소프트웨어공학을 잘 이해하고 있는 개발자의 자세입니다ㅋㅋㅋ

Flask-paginate

제가 왜 Flask인가?라는 예전 포스트에서, FastAPI가 탐나긴 하지만 Flask로 이 블로그를 만든 이유가 바로 여러가지 공부하기 좋은 한글 레퍼런스가 많아서라고 했었죠. Flask에는 Flask-paginate라는 페이지네이션을 쉽게 구현할 수 있는 좋은 라이브러리가 있고, 제가 처음 Flask를 배웠던 점프 투 플라스크 강의에 Flask-paginate 한글 레퍼런스도 있습니다. 이 블로그의 페이지네이션도, 한글 레퍼런스를 토대로 Flask-paginate 공식문서를 참조해가며 만들었습니다.

HTML template

Python 코드보다는 html template부터 보는게 마음이 편하겠죠, 아래는 이 블로그에서 페이지네이션을 구현한 html 코드입니다. 댓글 표시나 저자 표시 등 기능이 몇 가지 더 있지만, 페이지네이션과 관련없는 부분은 이해를 위해 일단 제거했습니다.

{% for post in posts %}
<article>
    <a href="{{ url_for('blog.detail', id=post['id'])}}">
        {{ post['title'] }}
    </a>
</article>
{% endfor %}
{{ pagination.links | safe }}

생각보다 html에서 구현할 부분은 많지 않죠? Flask-paginate를 사용하지 않았다면 아마 조금 더 복잡해졌을겁니다. 여기서 주목할 부분은 두 가지가 있습니다.

  1. {% for post in posts %} 반복문: Jinja 언어로 구현된 이 반복문에서는, Flask에서 보내준 posts를 순회하면서 post를 가져옵니다. 만약 페이지네이션을 해주지 않았다면 posts는 몇십 개겠지만, 10개씩 페이지네이션을 해준다고 치면 posts는 항상 10개 길이일 것입니다.
  2. pagination.links: Flask-paginate에서 pagination 객체를 만들어서 넘겨주면, 객체가 갖고있는 links라는 attribute가 바로 우리가 원하는 그 각 페이지로 갈 수 있는 링크 부분입니다. 내부적으로는 우리가 구현하고싶은 html 덩어리를 추상화해놓았습니다. 만약 Flask-paginate를 사용하지 않았다면 여기가 엄청 복잡해졌겠죠. 다만, safe를 걸어주어서, 안전한 객체가 전달되었으니 브라우저 상에서 실행해도 괜찮다~ 라고 브라우저를 안심시켜줘야 html 위에 보이게 할 수 있습니다.

Python 구현부

그럼 저 pagination.links 하나를 만들기 위해 Flask에서는 무엇을 해줘야할까요? 아래는 제가 이 블로그에서 페이지네이션을 구현한 Python 코드입니다. 코드를 따라가보면서 Flask-paginate의 인터페이스를 확인해봅시다.

from flask_paginate import Pagination, get_page_args

@BP.route("/", methods=("GET",))  # index 페이지를 호출하면
def index():
    per_page = 10
    page, _, offset = get_page_args(per_page=per_page)  # 포스트 10개씩 페이지네이션을 하겠다.
    # 이 때 두 번째 return값은 per_page입니다.
    # 저는 per_page를 따로 get_page_args에 넣어줘서, per_page를 받아서 사용하지는 않았습니다.
    # page는 현재 위치한 page입니다. 기본적으로 1이고, 페이지 링크를 누르면 2, 3, ...입니다.
    # offset은 page에 따라 몇 번째 post부터 보여줄지입니다.
    # 기본적으로 0이고, 2페이지라면 10, 3페이지라면 20이겠죠.

    cur = get_cur()
        cur.execute("SELECT COUNT(*) FROM posts;")  # 일단 총 몇 개의 포스트가 있는지를 알아야합니다.
        total = cur.fetchone()[0]
    cur.execute(
        "SELECT * FROM posts ORDER BY created "  # SQL SELECT로 포스트를 가져오되,
        "DESC LIMIT %s OFFSET %s;",  # offset부터 per_page만큼의 포스트를 가져옵니다.
        (per_page, offset),
    )
    posts = cur.fetchall()

    return render_template(
        "blog/index.html",
        posts=posts,
        pagination=Pagination(
            page=page,  # 지금 우리가 보여줄 페이지는 1 또는 2, 3, 4, ... 페이지인데,
            total=total,  # 총 몇 개의 포스트인지를 미리 알려주고,
            per_page=per_page,  # 한 페이지당 몇 개의 포스트를 보여줄지 알려주고,
            prev_label="<<",  # 전 페이지와,
            next_label=">>",  # 후 페이지로 가는 링크의 버튼 모양을 알려주고,
            format_total=True,  # 총 몇 개의 포스트 중 몇 개의 포스트를 보여주고있는지 시각화,
        ),
        search=True,  # 페이지 검색 기능을 주고,
        bs_version=5,  # Bootstrap 사용시 이를 활용할 수 있게 버전을 알려줍니다.
    )

이를 요약하면 다음과 같은 단계로 진행됩니다.

  1. Flask에 N번째 페이지(page)를 보여달라는 요청이 들어옵니다.
  2. N번째 페이지라면 10 * (N-1) 번째 포스트(offset)부터 보여줘야겠죠, 예를 들면 1번 페이지면 0번 포스트, 2번 페이지면 10번 포스트처럼요. 그걸 get_page_args()함수로 계산해옵니다.
  3. SQL로 DB에 있는 포스트를 가져올 때, 10 * (N-1) 번째 포스트에서부터 10개만큼 가져오도록 LIMIT와 OFFSET을 줍니다.
  4. render_template()할 때, Pagination객체를 같이 전달해서, html에서 뭘 잘 몰라도 이 객체에서 전달해준 페이지 수라거나, 기호라거나, 내장된 html 형태 등을 잘 사용하게끔 돕습니다.

약간의 커스터마이징 팁

이 때 prev_label/next_label은 링크의 버튼 모양으로 미리 설정된 html에서의 문자를 바꿔줄 수 있고, format_total=True를 설정해준 다음 html에서 {{ pagination.info }}라는 attribute를 사용해주면 Total 25 records, displaying 11-20과 같은 글자를 보여줄 수 있습니다. 저는 디자인이 맘에 안들어서 사용하고 있지 않지만, Flask-paginate 공식문서를 참조해보세요.

그리고 저는 css로 Pagination 객체에서 제공해주는 html 영역을 입맛에 맞춰 바꿔서 사용하고 있습니다. 특히 파란색을 적극적으로 사용하는 기본 디자인을, Bootstrap과 잘어울리게 검은색을 좀 사용하게끔 아래와 같이 바꾸었습니다.

.pagination a { text-decoration: none; color: inherit;}
.page-item .page-link:hover { color: #6c757d; }
.page-item.active .page-link { background-color: #6c757d; border-color: #6c757d; }
.page-item.active .page-link:hover { color: white; }

Flask-paginate는 잘 만든 라이브러리이지만, 분명 디자인적 측면에서 추상화된 부분이 너무 많아 이를 커스터마이즈할 수 있음에도 불구하고 아쉬움이 약간 있습니다. 저는 언젠가 Bootstrap Pagination을 적극적으로 활용해서 직접 Flask에서 페이지네이션을 구현해볼 생각이니, 그 내용도 업데이트되면 포스팅해보도록 하겠습니다.

댓글