발행일

페이지네이션 가독성 리디자인 회고 — 파란색 테마 위의 활성 상태 표현

페이지네이션 가독성 리디자인 회고

게시판 페이지네이션을 Bootstrap 기본 스타일로 붙여뒀더니, 실제 화면에서 활성 페이지 번호가 전혀 눈에 띄지 않았습니다. 원인을 파보니 사이트 테마와의 충돌이었고, 결국 전면 리디자인으로 풀었습니다. 그 과정을 정리합니다.


1. 증상 — 현재 페이지가 어디인지 모르겠다

Bootstrap의 기본 페이지네이션은 활성 상태에 background-color: var(--bs-primary) 를 씁니다. 기본값은 파란색 계열이죠.

문제는 이 서비스의 브랜드 컬러 자체가 파란색이었다는 점입니다. 헤더, 링크, 주요 버튼이 전부 파란색이라 활성 페이지 번호도 파란 배경에 흰 숫자로 나오는데, 주변의 비활성 페이지 번호 역시 파란 글자라 색상 대비가 거의 사라졌습니다.

<!-- 기본 Bootstrap -->
<ul class="pagination">
  <li class="page-item"><a class="page-link">1</a></li>
  <li class="page-item active"><a class="page-link">2</a></li>
  <li class="page-item"><a class="page-link">3</a></li>
</ul>

사용자 피드백:

"지금 몇 페이지에 있는지 한눈에 안 들어와요."

"이전/다음 버튼도 그냥 텍스트라 클릭 가능한지 헷갈려요."


2. 원인 분석

색만 바꾸는 걸로는 안 되겠다 싶어 문제를 세 갈래로 쪼갰습니다.

2.1 색상 충돌

활성 페이지의 파란색이 사이트의 어떤 파란색인지 구별이 안 됐습니다. 헤더도 파랑, 링크도 파랑, 활성 페이지도 파랑.

2.2 모양 차이 없음

활성/비활성이 똑같은 직사각형에 색만 반전된 형태입니다. 시각적 hierarchy가 색 하나에만 매달려 있었습니다.

2.3 "이전/다음"의 클릭 가능성 애매

< > 문자만 있어 버튼이라기보다 구분자처럼 보였습니다. 비활성 상태(첫/마지막 페이지)에서는 회색 텍스트라 더 그랬습니다.


3. 리디자인 방향

색상 하나가 아니라 형태·그림자·아이콘 세 축을 동시에 써서 hierarchy를 만들기로 했습니다.

요소비활성호버활성
배경흰색연한 회색진한 파란색
테두리회색 1px진한 회색 1px없음
그림자없음얕은 그림자진한 그림자
글자진한 회색검정흰색
모양평면약간 떠보임카드처럼 떠있음

"카드형"이라 부른 이유는 활성 페이지가 약간 떠올라 보이는 입체감을 줬기 때문입니다. 그림자가 있으면 색만 볼 때보다 "이게 지금 위치"라는 게 훨씬 빠르게 인지됩니다.


4. 구현

Bootstrap 기본 클래스를 오버라이드하는 대신 별도 클래스를 만들어 섞었습니다. 유지보수할 때 "내가 건드린 것"과 "Bootstrap 기본"을 구분하려는 의도였습니다.

/* static/css/pagination.css */
.pagination--card .page-link {
  min-width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #d9d9d9;
  border-radius: 8px;
  color: #333;
  background: #fff;
  margin: 0 4px;
  transition: all 0.15s ease;
}

.pagination--card .page-link:hover {
  background: #f5f5f5;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  color: #000;
}

.pagination--card .page-item.active .page-link {
  background: #2a4dd0;       /* 사이트 파랑보다 한 톤 진하게 */
  border-color: transparent;
  color: #fff;
  box-shadow: 0 4px 12px rgba(42, 77, 208, 0.35);
  transform: translateY(-1px);
}

.pagination--card .page-item.disabled .page-link {
  background: #f7f7f7;
  color: #bbb;
}

포인트:

  • 활성 색은 브랜드 파랑보다 한 톤 진하게 잡았습니다. 완전히 다른 색으로 바꾸면 브랜드 일관성이 깨지고, 똑같이 쓰면 본래 문제로 되돌아갑니다.
  • transform: translateY(-1px) — 1px만 띄워도 "지금 여기"가 확 살아납니다. 과하게 띄우면 버튼이 잘려 보이니 조심해야 합니다.
  • min-width — 숫자 페이지와 아이콘 페이지(<, >)의 너비를 통일해 라인이 흔들리지 않게 했습니다.

5. 이전/다음 버튼 — 텍스트에서 아이콘으로

<, > 문자는 폰트마다 다르게 그려지고 안티에일리어싱이 어중간할 때 삐뚤어 보입니다. 아이콘으로 교체했습니다.

<li class="page-item {% if not page.has_previous %}disabled{% endif %}">
  <a class="page-link" href="?page={{ page.previous_page_number }}" aria-label="이전">
    <i class="bi bi-chevron-left"></i>
  </a>
</li>
...
<li class="page-item {% if not page.has_next %}disabled{% endif %}">
  <a class="page-link" href="?page={{ page.next_page_number }}" aria-label="다음">
    <i class="bi bi-chevron-right"></i>
  </a>
</li>
  • aria-label — 아이콘만 있으면 스크린 리더가 읽을 게 없습니다. "이전", "다음"을 명시합니다.
  • <&lt; 로 직접 쓸 때의 위험: 템플릿 엔진에 따라 이스케이프 동작이 달라집니다. 아이콘 쪽이 안전합니다.

6. 페이지 범위 표시 — "1 ... 4 5 6 ... 20"

글이 많아지면 페이지 번호가 화면을 꽉 채워서, 현재 페이지 기준 앞뒤 2개씩만 보여주고 나머지는 ... 으로 축약했습니다.

# board/templatetags/pagination_extras.py
from django import template

register = template.Library()


@register.simple_tag
def pagination_range(page_obj, window=2):
    current = page_obj.number
    total = page_obj.paginator.num_pages
    start = max(1, current - window)
    end = min(total, current + window)
    pages = list(range(start, end + 1))
    if start > 1:
        pages = [1, '...'] + pages
    if end < total:
        pages = pages + ['...', total]
    return pages

템플릿에서는 '...' 이면 클릭 불가한 span으로, 숫자면 링크로 렌더링합니다. 이 패턴은 어느 프레임워크에 갖다 붙여도 동일하게 돌아갑니다.


7. 검증

항목결과
활성 페이지 인지색·그림자·1px 부양이 동시에 적용돼 한눈에 구분
이전/다음아이콘으로 클릭 가능함이 드러남, 첫/마지막에서 disabled
라인 흔들림min-width: 40px로 숫자·아이콘 폭 통일, 페이지 이동 시 위치 고정
스크린 리더이전·다음에 aria-label 적용
축약페이지가 많아도 한 줄 유지

8. 지금 다시 보면 — 두 가지 결함

...이 아무것도 생략하지 않고 표시되는 경우가 있습니다.

6절의 pagination_range를 다시 보면 조건이 이렇습니다.

if start > 1:
    pages = [1, '...'] + pages

window=2, 현재 페이지가 4라고 해봅시다.

start = max(1, 4-2) = 2
end   = min(total, 4+2) = 6
pages = [2, 3, 4, 5, 6]

start > 1 이므로 → [1, '...', 2, 3, 4, 5, 6]

1과 2 사이에는 생략된 페이지가 없는데 ...이 들어갑니다. 화면에는 1 … 2 3 4 5 6처럼 나와요. 마지막 쪽도 똑같아서, endtotal - 1이면 … 20 앞에 아무것도 안 생략된 채 점이 붙습니다.

조건을 하나씩 밀면 됩니다.

if start > 2:                       # 1과 start 사이에 실제로 빈 구간이 있을 때만
    pages = [1, '...'] + pages
elif start == 2:
    pages = [1] + pages             # 바로 이어지면 점 없이 1만

if end < total - 1:
    pages = pages + ['...', total]
elif end == total - 1:
    pages = pages + [total]

기능이 망가지는 버그는 아니라 눈에 안 띄었는데, 점 세 개는 "여기 뭔가 생략됐다"는 약속입니다. 아무것도 안 생략하고 점을 찍으면 그 약속이 깨져요. 이 글이 처음부터 "기능은 되는데 안 보이는 것도 버그" 를 주제로 잡았으면서, 정작 같은 종류를 하나 남겨둔 셈입니다.

② 색 대비를 눈으로만 판단했습니다.

3절에서 "색 하나에만 매달리지 말자" 고 해놓고, 정작 대비를 숫자로 재지 않았습니다. 특히 비활성 상태가 걸려요.

.pagination--card .page-item.disabled .page-link {
  background: #f7f7f7;
  color: #bbb;          /* 거의 흰 배경 위의 밝은 회색 */
}

비활성 요소라 대비 요구가 낮긴 하지만, "클릭할 수 없다"는 것과 "읽을 수 없다"는 다릅니다. 첫 페이지에서 이전 버튼이 보이긴 해야 상태를 인지하니까요.

그리고 형태 축이라고 넣은 것들(그림자, translateY(-1px))은 색을 못 보는 상황에서는 거의 도움이 안 됩니다. 흑백 출력이나 색각 이상 사용자에게는 1px 이동과 옅은 그림자가 잘 안 보여요. 정말 색에 안 기대려면 테두리 굵기나 밑줄처럼 명암만으로 구분되는 축이 하나 있어야 했습니다.


9. 회고 — "기능은 되는데 안 보이는" 것도 버그다

처음에는 이걸 "사소한 CSS 이슈"로 미뤄뒀습니다. 클릭하면 페이지가 넘어가니까요. 그런데 실제로 사용자 테스트를 해 보니 "지금 몇 페이지인지 모르겠다"는 피드백이 가장 먼저 나왔습니다. 기능이 완성이어도 현재 상태를 인지할 수 없으면 그건 실패입니다.

작업하며 챙긴 메모:

  • 색 하나로 상태를 표현하면 테마와 충돌하기 쉽습니다. 형태·그림자·아이콘을 보조 축으로 가져가면 훨씬 안전합니다.
  • Bootstrap 기본 스타일을 오버라이드할 때는 별도 클래스로. .page-item.active 를 직접 덮어쓰지 말고 .pagination--card .page-item.active 식으로 스코프를 좁히는 편이 유지보수에 유리합니다.
  • aria-label은 기능 구현의 일부입니다. 아이콘을 넣는 순간 세트로 들어가야 합니다.

기능이 완성된 다음에 오히려 "지금 여기가 어디인지 보여주는" 작은 UI가 체감 품질을 크게 끌어올린다는 게 이번 작업의 수확이었습니다.