본문 바로가기

UI

[ UI / pagination ] 어떤 타입의 페이지네이션들이 있을까?

최근 게시판을 만들어볼려고 작업을 하고 있었다.
게시판은 정말로 간단한 작업이다. 그렇기 때문에 특별한 기술은 필요없다.

그럼에도 게시판에서도 나름의 UX를 고민해보고 싶어서'페이지네이션(pagination)'에 고민을 해 보았다.

하여, AI의 도움을 받아 TOP3에 해당하는 페이지네이션 UI를 보고 장단점을 고민해 보자.

 

 

페이지네이션 목록

1. 네비게이션 타입

2. 컨텍스트 타입

3. 하이브리드 점프 타입

 


 

1. 네비게이션 타입

가장 많이 쓰이는 타입 중 하나라고 생각한다.
이 페이지네이션의 핵심은 처음과 맨 마지막 중간즘에 있을때,
가운데 3개를 제외하고 양 옆에 ... 처리하는 방식이다.

장점 단점
  • 처음/이전/다음/마지막 페이지로의 완전한 네비게이션 제공
  • 현재 위치와 전체 범위를 명확히 표시하여 사용자 방향성 제공
  • 접근성이 우수함 (ARIA 레이블, 키보드 지원)
  • 아이템 수와 현재 범위를 명확히 표시
  • 비활성화 상태의 시각적 피드백 제공
  • 많은 컨트롤로 인한 UI 복잡도 증가
  • 모바일에서 터치 타겟이 다소 작을 수 있음
  • 첫/마지막 페이지 버튼이 실수로 클릭될 수 있음
  • 화면 공간을 비교적 많이 차지
 

 


const getSmartRange = (current, total) => {
const numbers = [];
const addNumber = (num) => numbers.push(num);
const addEllipsis = () => numbers.push('...');

addNumber(1);

if (current <= 4) {
  [2, 3, 4].forEach(addNumber);
} else {
  addEllipsis();
  addNumber(current - 1);
  addNumber(current);
}

if (current > 4 && current < total - 3) {
  addNumber(current + 1);
  addEllipsis();
} else if (current <= 4) {
  addEllipsis();
} else {
  [total - 2, total - 1].forEach(addNumber);
}

addNumber(total);
return numbers;
};


<section className="space-y-4">
        <div className="border rounded-lg p-4">
          {generateItems(currentPage).map(item => (
            <div key={item.id} className="py-2 border-b last:border-0">
              {item.title}
            </div>
          ))}
          <nav 
            className="flex items-center justify-center gap-2 mt-4" 
            role="navigation" 
            aria-label="Pagination"
          >
            <button
              onClick={() => setCurrentPage(1)}
              disabled={currentPage === 1}
              className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50"
              aria-label="First page"
            >
              <ChevronsLeft className="w-4 h-4" />
            </button>
            <button
              onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
              disabled={currentPage === 1}
              className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50"
              aria-label="Previous page"
            >
              <ChevronLeft className="w-4 h-4" />
            </button>
            
            {getSmartRange(currentPage, totalPages).map((num, idx) => (
              <button
                key={idx}
                onClick={() => typeof num === 'number' && setCurrentPage(num)}
                className={`w-10 h-10 rounded-lg ${
                  currentPage === num 
                    ? 'bg-blue-500 text-white' 
                    : typeof num === 'number' ? 'hover:bg-gray-100' : ''
                }`}
                aria-current={currentPage === num ? 'page' : undefined}
                disabled={typeof num !== 'number'}
              >
                {num}
              </button>
            ))}
            
            <button
              onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
              disabled={currentPage === totalPages}
              className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50"
              aria-label="Next page"
            >
              <ChevronRight className="w-4 h-4" />
            </button>
            <button
              onClick={() => setCurrentPage(totalPages)}
              disabled={currentPage === totalPages}
              className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50"
              aria-label="Last page"
            >
              <ChevronsRight className="w-4 h-4" />
            </button>
          </nav>
          <div className="text-center text-sm text-gray-500 mt-2">
            {`${(currentPage - 1) * itemsPerPage + 1} - ${Math.min(currentPage * itemsPerPage, totalPages * itemsPerPage)} of ${totalPages * itemsPerPage} items`}
          </div>
        </div>
      </section>

 

 

2. 컨텍스트 인식 페이지네이션

현재 내가 어디에 있는지 알 수 있는 컨텍스트 페이지네이션이다.
가장 기본이라고 할 수 있다.

장점 단점
  • 다음/이전 페이지의 컨텐츠 정보 제공
  • 직관적이고 단순한 UI
  • 모바일 친화적인 터치 영역
  • 불필요한 정보를 제거한 깔끔한 디자인
  • 원하는 페이지로 직접 이동 불가
  • 많은 페이지가 있을 때 탐색이 느림
  • 전체 범위 파악이 어려울 수 있음
  • 페이지 번호만으로는 컨텐츠 예측이 어려움

 

<section className="space-y-4">
       
        <div className="border rounded-lg p-4">
          {generateItems(currentPage).map(item => (
            <div key={item.id} className="py-2 border-b last:border-0">
              {item.title}
            </div>
          ))}
          <div className="flex items-center justify-between mt-4 text-sm">
            <button
              onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
              className="flex items-center gap-2 px-4 py-2 rounded-lg hover:bg-gray-100"
              disabled={currentPage === 1}
            >
              <ChevronLeft className="w-4 h-4" />
              {currentPage > 1 && `이전 ${itemsPerPage}개 항목`}
            </button>
            
            <span className="text-gray-500">
              페이지 {currentPage} / {totalPages}
            </span>
            
            <button
              onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
              className="flex items-center gap-2 px-4 py-2 rounded-lg hover:bg-gray-100"
              disabled={currentPage === totalPages}
            >
              {currentPage < totalPages && `다음 ${itemsPerPage}개 항목`}
              <ChevronRight className="w-4 h-4" />
            </button>
          </div>
        </div>
      </section>

 

3. 하이브리드 점프

페이지 이번 영역을 통해서 한번에 페이지로 이동할 수 있는 장점이있다.
만약 내가 어떤 페이지에 어떤 콘텐츠가 있다는 것을 알고 있다면 굳이
좌우 버튼을 클릭해서 이동할 필요가 없어 좋다.

장점 단점
  • 직접 페이지 입력 기능으로 빠른 이동 가능
  • 컴팩트한 UI로 공간 효율적
  • 모달을 통한 명확한 사용자 인터랙션
  • 일반 페이지네이션과 점프 기능의 결합
  • 추가적인 클릭이 필요함 (모달 열기)
  • 모달로 인한 컨텍스트 전환 발생
  • 잘못된 페이지 번호 입력 가능성
  • 모바일에서 숫자 입력이 불편할 수 있음

 

 

<section className="space-y-4">

        <div className="border rounded-lg p-4">
          {generateItems(currentPage).map(item => (
            <div key={item.id} className="py-2 border-b last:border-0">
              {item.title}
            </div>
          ))}
          <div className="flex items-center justify-between mt-4">
            <button
              onClick={() => setShowJumpDialog(!showJumpDialog)}
              className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg flex items-center gap-2"
            >
              <Search className="w-4 h-4" />
              페이지 이동
            </button>
            
            {showJumpDialog && (
              <div className="absolute mt-2 p-4 bg-white border rounded-lg shadow-lg">
                <div className="flex items-center gap-2">
                  <input
                    type="number"
                    min="1"
                    max={totalPages}
                    value={jumpToPage}
                    onChange={(e) => setJumpToPage(e.target.value)}
                    className="w-20 px-2 py-1 border rounded"
                    placeholder="페이지"
                  />
                  <button
                    onClick={() => {
                      const page = parseInt(jumpToPage);
                      if (page >= 1 && page <= totalPages) {
                        setCurrentPage(page);
                        setShowJumpDialog(false);
                        setJumpToPage('');
                      }
                    }}
                    className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
                  >
                    이동
                  </button>
                </div>
              </div>
            )}
            
            <div className="flex items-center gap-2">
              {getSmartRange(currentPage, totalPages).map((num, idx) => (
                <button
                  key={idx}
                  onClick={() => typeof num === 'number' && setCurrentPage(num)}
                  className={`w-8 h-8 rounded ${
                    currentPage === num 
                      ? 'bg-blue-500 text-white' 
                      : typeof num === 'number' ? 'hover:bg-gray-100' : ''
                  }`}
                >
                  {num}
                </button>
              ))}
            </div>
          </div>
        </div>
      </section>

 

 

각각의 페이지네이션에는 장점이 있다. 따라서 장점에 맞춰서 페이지네이션을 선택하면 좋을 거 같다.
고객이 원하는 것이 없다면, 기본을 선택하면 좋다.
다만, 게시판의 특성을 파악할 수 있다면 조금 더 세부적인 선택이 가능하다고 생각하다.

어떤 게시판에서 페이지네이션을 무한 스크롤로 만들기도 한다.
꼭 위의 내용이 전부는 아니다. 그럼에도 무조건 만들기보다는 고민을 해보는 게 좋은 거 같다.

내가 현재 어떤 페이지를 만들고 있고 어떤 UX가 사용자에게 더 사용성을 높여줄 것인지.