Pagination | CoUI
본문으로 건너뛰기
LogoCoUI

Pagination

대용량 콘텐츠를 여러 페이지로 나누어 탐색하는 페이지네이션 컴포넌트

목차

대용량 콘텐츠를 페이지 단위로 나누어 탐색할 수 있는 컴포넌트입니다. 현재 페이지 표시, 이전/다음 이동, 첫/마지막 페이지 이동을 지원합니다.

기본 사용법 (Basic Usage)#

Flutter / Web 모두 동일한 Pagination API 를 사용합니다.

// 기본 페이지네이션
Pagination(
  page: _currentPage,
  totalPages: 10,
  onPageChanged: handlePageChanged,
)

// 더 넓은 페이지 범위 + prev/next 라벨
Pagination(
  page: _currentPage,
  totalPages: 20,
  onPageChanged: handlePageChanged,
  maxPages: 7,
  showLabel: true,
)

// 첫 페이지에서 이전 버튼 숨김
Pagination(
  page: _currentPage,
  totalPages: 5,
  onPageChanged: handlePageChanged,
  hidePreviousOnFirstPage: true,
)
// 기본 페이지네이션 (이전/다음 버튼 포함)
Pagination(
  page: currentPage,
  totalPages: 10,
  onPageChanged: handlePageChanged,
)

// 더 넓은 페이지 범위 + prev/next 라벨
Pagination(
  page: currentPage,
  totalPages: 20,
  onPageChanged: handlePageChanged,
  maxPages: 7,
  showLabel: true,
)

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 테이블, 검색 결과, 상품 목록처럼 많은 데이터를 페이지 단위로 나눠 표시할 때
  • 사용자가 특정 페이지로 직접 이동할 수 있어야 할 때
  • 전체 데이터 양이 많지 않아 페이지 번호로 탐색이 유용한 경우

대신 다른 컴포넌트를 사용하세요:

  • RefreshTrigger: 무한 스크롤 방식으로 데이터를 로드할 때
  • Select: 페이지 크기 선택 등 드롭다운 옵션이 필요할 때

Props / Parameters#

속성타입기본값설명
pageint필수현재 페이지 (1-based)
totalPagesint필수전체 페이지 수
onPageChanged void Function(int)? null 페이지 변경 핸들러 (1-based)
enabled bool? null null 이면 onPageChanged != null 에서 유도 ( enabled ?? (onPageChanged != null) ). 비활성이면 모든 숫자 · 말줄임 · 화살표가 disabledColor , 현재 페이지는 disabledBackgroundColor 위에 그려지고 어떤 버튼도 상호작용하지 않습니다
maxPages int 3 표시할 최대 페이지 버튼 수
showSkipToFirstPage bool true 선행 생략 시 첫 페이지 이동 버튼 표시
showSkipToLastPage bool true 후행 생략 시 마지막 페이지 이동 버튼 표시
hidePreviousOnFirstPage bool false 첫 페이지에서 이전 버튼 숨김
hideNextOnLastPage bool false 마지막 페이지에서 다음 버튼 숨김
showLabel bool false 이전/다음 버튼에 텍스트 라벨 표시
variant CorePaginationVariant standard 시각 변형
role CoreSurfaceRole defaultRole 표면 재질 선택 역할. 접근성 navigation 역할과 별개
paginationStyle CorePaginationStyle? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

Pagination 의 모든 chrome / dimensional 오버라이드는 CorePaginationStyle 단일 슬롯으로 흐릅니다. behaviour 필드 (page / totalPages / onPageChanged / maxPages / showSkipToFirstPage / showSkipToLastPage / hidePreviousOnFirstPage / hideNextOnLastPage / showLabel) 는 위젯/컴포넌트 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯/컴포넌트 파라미터로 직접 (page, totalPages, onPageChanged, maxPages, showSkipToFirstPage, showSkipToLastPage, hidePreviousOnFirstPage, hideNextOnLastPage, showLabel)
  • chrome / dimensional: CorePaginationStyle 한 곳으로

Resolve chain#

CorePaginationStyle.defaultX                   // design system default
  → CorePaginationTheme.style                  // 프로젝트 공통
  → widget.paginationStyle                     // 인스턴스별

CorePaginationStyle 필드#

필드타입설명
itemSpacing double? Spacing between adjacent pagination buttons (logical px), applied as the host-layout Row.spacing (Flutter) / CSS flex gap (Web) of the outer pagination row.
buttonSize double? Width / height of a square pagination button (logical px).
buttonRadius CoreBorderRadius? Corner radius of a pagination button.
labelButtonPadding CoreEdgeInsets? Inner padding of the previous / next buttons in label mode ( showLabel: true ), whose width follows their content. Defaults to [defaultLabelButtonPadding].
buttonTextStyle CoreTextStyle? Base text style override shared by every page-number / prev / next button (typography role + weight; carries no per-state colour). Layered on top of [defaultButtonTextStyle].
activeButtonTextStyle CoreTextStyle? Typography override of the active (current) page button, layered on top of [buttonTextStyle] and [defaultActiveButtonTextStyle]. Carries no colour — that is [activeTextStyle].
activeBackgroundColor CoreColor? Background colour of the active (current) page button.
activeTextStyle CoreTextStyle? Text colour override for the active (current) page button, carried via [CoreTextStyle.color] inside this slot. Defaults to [defaultActiveTextStyle]. The active button's typography is [activeButtonTextStyle], not this slot.
hoverBackgroundColor CoreColor? Background colour applied to a button on hover.
hoverTextStyle CoreTextStyle? Text colour override applied to a button on hover, carried via [CoreTextStyle.color] inside this slot. Defaults to [defaultHoverTextStyle].
textStyle CoreTextStyle? Text colour override for an inactive / prev / next button, carried via [CoreTextStyle.color] inside this slot. The chevrons take it too. Defaults to [defaultTextStyle].
disabledColor CoreColor? Override colour of every number, the … and the chevrons while the pagination is disabled. null defers to [defaultDisabledColor].
disabledBackgroundColor CoreColor? Override fill of the active (current) page button while the pagination is disabled. null defers to [defaultDisabledBackgroundColor].
iconStyle CoreIconStyle? Style of the previous / next chevron (its size; the colour is the button's foreground unless this slot states one). null defers to [defaultIconStyle].
hoverDuration Duration? Colour-transition duration for button hover. null → token default ([defaultHoverDuration]).
ellipsisStyle CoreTextStyle? Ellipsis ( … ) text style override, layered on top of [defaultEllipsisStyle] — colour only by default, so the … takes the shared button typography ([buttonTextStyle]) and differs from a page number by its colour alone. A typography overlay stated here is applied to the … alone.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed per-button Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.

빠른 오버라이드 (Chain)#

이미 만든 Pagination 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class PaginationChainExample extends StatefulWidget {
  const PaginationChainExample({super.key});

  @override
  State<PaginationChainExample> createState() => _PaginationChainExampleState();
}

class _PaginationChainExampleState extends State<PaginationChainExample> {
  int _page = 3;

  @override
  Widget build(BuildContext context) {
    return Pagination(
      page: _page,
      totalPages: 10,
      onPageChanged: (p) => setState(() => _page = p),
    ).withStyle(
      const CorePaginationStyle(
        itemSpacing: CoreSpace.space8,
        buttonSize: CoreSize.size40,
        buttonRadius: CoreBorderRadius.all(CoreRadius.radius16),
        activeBackgroundColor: CoreColor.token(CoreColors.tertiary),
      ),
    );
  }
}
class PaginationChainExample extends StatefulComponent {
  const PaginationChainExample({super.key});

  @override
  State<PaginationChainExample> createState() => _PaginationChainExampleState();
}

class _PaginationChainExampleState extends State<PaginationChainExample> {
  int _page = 3;

  @override
  Component build(BuildContext context) {
    return Pagination(
      page: _page,
      totalPages: 10,
      onPageChanged: (value) => setState(() => _page = value),
    ).withStyle(
      const CorePaginationStyle(
        itemSpacing: CoreSpace.space8,
        buttonSize: CoreSize.size40,
        buttonRadius: CoreBorderRadius.all(CoreRadius.radius16),
        activeBackgroundColor: CoreColor.token(CoreColors.tertiary),
      ),
    );
  }
}

변형 (Variants)#

Pagination 은 현재 단일 시각 변형(standard) 을 가집니다. 표시되는 페이지 범위와 prev/next 라벨은 behaviour 파라미터로 조정합니다.

// 더 넓은 페이지 범위 + 라벨
Pagination(
  page: 5,
  totalPages: 20,
  onPageChanged: handlePageChanged,
  maxPages: 7,
  showLabel: true,
)

동작 스펙 (Behavior)#

인터랙션#

  • 페이지 버튼 클릭: 해당 페이지로 이동 (onPageChanged(page) 호출)
  • 이전/다음 버튼: 현재 페이지에서 1씩 감소/증가
  • 첫/마지막 버튼: 1페이지 또는 totalPages로 이동
  • 현재 페이지 버튼: 클릭해도 이벤트 없음 (비활성 강조)
  • 경계 버튼: 첫 페이지에서 이전 버튼, 마지막 페이지에서 다음 버튼은 비활성
  • onPageChanged 가 없으면 enabled 도 false 로 유도되어 전체가 비활성으로 그려집니다. 핸들러 없이 평소 색으로 표시만 하려면 enabled: true 를 명시합니다

상태 전환#

  • 현재 페이지 버튼: primary 채움 · onPrimary · labelLarge 14 SemiBold. 나머지 숫자는 bodyMedium 14 Regular · onSurfaceVariant, 호버하면 onSurface 와 surfaceContainerHigh 채움
  • 말줄임(…)은 숫자와 같은 타이포그래피에 onSurfaceSubtle, 이전/다음 화살표는 16 · 숫자와 같은 색
  • 버튼은 36 정사각형(모서리 8), 버튼 사이 간격은 4. 포커스 링은 버튼 모서리 8 에 링의 reach 4 를 더한 12, 높이 44
  • 비활성(enabled: false): 모든 숫자 · 말줄임 · 화살표가 disabledColor(disabled-content), 현재 페이지는 disabledBackgroundColor(disabled-container) 채움. 호버 · 포커스 · 탭에 반응하지 않고 Clickable 의 반투명 처리도 겹쳐 입히지 않습니다
  • 경계 도달 시 이전/다음 버튼 disabled 처리
  • 페이지 수가 많으면 현재 페이지 기준으로 중간 항목 ...으로 생략

가용 폭에 맞추기#

행이 놓인 폭보다 넓어지면 넘치지 않고 아래 순서로 버튼을 내려놓는다. 각 단계는 앞 단계를 유지한 채 하나를 더 내려놓고, 행은 들어가는 첫 단계로 그려진다 (CorePaginationLayout.ladder — 두 플랫폼이 같은 표를 읽는다).

  1. 이전/다음 라벨 → 아이콘만 (라벨은 접근성 이름으로 남는다)
  2. 첫/마지막 페이지 바로가기 버튼
  3. 페이지 번호 창 → 현재 페이지 하나
  4. 생략(…) 버튼 → 이전 · 현재 · 다음

폭이 넉넉하면 다시 넓은 단계로 돌아간다. 크롬 값(버튼 크기 · 간격 · 패딩)은 바뀌지 않고, 보이는 버튼의 수만 줄어든다.

애니메이션#

  • 페이지 전환 시 별도 애니메이션 없음 (콘텐츠 영역 전환은 부모가 처리)
  • 버튼 호버 시 배경색 200ms 전환

사용 가이드라인 (Usage Guidelines)#

총 페이지 수와 번호 기반 이동에는 Pagination을 사용합니다. 적은 수의 슬라이드 위치를 점으로 보여주려면 DotIndicator, 패널 이름으로 전환하려면 Tabs를 선택합니다. Table 등 콘텐츠의 페이지 분할·데이터 로드는 호출자가 처리합니다.

행·열 데이터 자체의 렌더링은 Table에서 구성합니다.

✅ Do#

많은 데이터에는 skip-to-first / skip-to-last 버튼 활용

Pagination(
  page: _currentPage,
  totalPages: 50,
  onPageChanged: handlePageChanged,
  showSkipToFirstPage: true,
  showSkipToLastPage: true,
)

50페이지 이상의 데이터에서 첫/마지막 페이지 버튼은 멀리 있는 페이지로의 빠른 이동을 지원한다.


❌ Don't#

페이지가 1개일 때 Pagination 표시

// ❌ 페이지가 1개뿐인데 Pagination 표시
Pagination(
  page: 1,
  totalPages: 1,
  onPageChanged: handlePageChanged,
)

페이지가 1개뿐이면 Pagination이 의미 없고 오히려 혼란을 준다. totalPages > 1인 경우에만 표시한다.

✅ Do#

좁은 화면에서는 maxPages 를 작게 유지

Pagination(
  page: _currentPage,
  totalPages: 20,
  onPageChanged: handlePageChanged,
  maxPages: 3,
)

maxPages 가 크면 버튼이 너무 많아 모바일 화면에서 줄바꿈이 발생한다. 3~5가 적당하다.


❌ Don't#

데이터가 적을 때 불필요하게 페이지네이션을 표시하지 마세요.

// ❌ 한 페이지에 다 들어오는데도 표시
Pagination(
  page: 1,
  totalPages: 1,
  onPageChanged: handlePageChanged,
)

단일 페이지라면 페이지네이션은 불필요한 UI 요소입니다. 총 항목 수가 페이지당 개수보다 적으면 숨기세요.

접근성 (Accessibility)#

역할#

FlutterWeb
루트 Semantics(container: true, label: …) — 랜드마크 role 없음 <div role="navigation" aria-label="…">
페이지 버튼 · 이전/다음 · 말줄임 Clickable 의 Semantics(button: true) 위에 컴포넌트가 Semantics(enabled:, selected:, button: true) Clickable 이 role="button" + tabindex="0" (활성) / aria-disabled="true" (비활성), 컴포넌트가 aria-label 을 얹습니다
현재 페이지selected: true 시맨틱 플래그aria-current="page"

루트 이름은 두 플랫폼 모두 paginationLabel 로케일 키에서 옵니다(한국어 기본값 "페이지 매기기", 영어 기본값 "Pagination"). Web 에서는 role 과 aria-label 이 caller attributes 를 병합한 뒤에 기록되므로 호출부에서 덮을 수 없습니다.

키보드#

키동작
Tab / Shift + Tab버튼 하나씩 앞/뒤로 이동 (버튼마다 tab stop)
Enter포커스된 버튼 활성화
Space포커스된 버튼 활성화
Arrow (←→) 컴포넌트는 처리하지 않습니다. 네이티브 Flutter 에서는 프레임워크 기본값이 방향 포커스 이동으로 해석하고, Flutter Web 과 Web(Jaspr) 에서는 각각 스크롤 / 무동작으로 갑니다
Home / End 없음 (첫 · 마지막 페이지로 가려면 showSkipToFirstPage / showSkipToLastPage 버튼으로 Tab 해야 합니다)

Enter / Space 활성화는 두 플랫폼 모두 합성된 Clickable 이 소유합니다.

스크린 리더#

  • 페이지 번호 버튼의 이름: Flutter 는 렌더된 숫자 텍스트 그대로("3"), Web 은 aria-label="Page 3" 입니다 — 문구가 다르고, Web 쪽은 로케일을 따르지 않습니다(아래 알려진 제약 참조).
  • 이전 / 다음 버튼: Web 은 buttonPrevious / buttonNext 로케일 값을 aria-label 로 붙입니다. Flutter 는 showLabel: true 일 때만 그 문구가 화면 텍스트로 들어가 이름이 되고, 기본값(showLabel: false)에서는 아이콘만 남아 이름이 없습니다.
  • 말줄임(…) 버튼: Web 은 paginationJumpToPage(N) 로케일 값(한국어 "N 페이지로 이동")을 aria-label 로 붙입니다. Flutter 는 … 문자만 읽히므로 어디로 가는 버튼인지 알 수 없습니다.
  • 현재 페이지: Flutter selected: true, Web aria-current="page". 현재 페이지 버튼도 계속 활성·포커스 가능하며, 눌러도 같은 인덱스로 콜백이 다시 발생합니다.
  • 비활성 상태: 첫 페이지의 이전 버튼과 마지막 페이지의 다음 버튼은 Flutter enabled: false, Web aria-disabled="true" 가 됩니다. 컴포넌트 전체가 비활성(enabled: false 또는 onPageChanged 없음)이면 모든 버튼이 같은 표시를 받고(Flutter Semantics(enabled: false), Web 버튼 role="button" + aria-disabled="true" · 루트 aria-disabled="true") aria-current="page" 는 현재 페이지에 남습니다.
  • live region 없음 — 페이지가 바뀌어도 그 변화 자체는 통보되지 않습니다. 버튼을 직접 눌렀다면 포커스가 그 버튼에 있어 새 상태가 읽히지만, 외부 코드가 page 를 바꾸면 조용히 바뀝니다.

포커스 관리#

  • 진입 / 이탈: 오버레이가 아니므로 마운트·언마운트 시 포커스를 옮기지 않습니다.
  • 트랩: 없습니다. Tab 은 마지막 버튼을 지나 바깥으로 나갑니다.
  • tab stop 수: 렌더된 버튼마다 하나씩 생깁니다(roving tabindex 아님). maxPages 기본값 3 기준으로 이전/다음 · 말줄임 · 건너뛰기 버튼까지 합쳐 최대 8~9 개의 tab stop 이 생깁니다. 비활성 버튼은 두 플랫폼 모두 포커스 대상에서 빠집니다.
  • 페이지 전환 시 포커스: 버튼 배열이 다시 계산되면서 방금 누른 버튼이 사라질 수 있습니다(예: 말줄임을 눌러 범위가 이동). 컴포넌트는 포커스를 재배치하지 않으므로 그 경우 포커스는 문서 기본 위치로 떨어집니다.
  • 포커스 표시: Clickable 이 소유합니다 — Flutter 는 FocusOutline, Web 은 :focus-visible 링입니다.

알려진 제약#

  • Web 의 페이지 번호 라벨이 영어로 고정되어 있습니다. aria-label 이 'Page $label' 문자열 리터럴로 만들어지고, 이에 대응하는 로컬라이제이션 키 자체가 없습니다 — 한국어 로케일에서도 "Page 3" 으로 읽힙니다(같은 컴포넌트의 이전/다음·말줄임 라벨은 로케일을 따릅니다). 대안: 없습니다 — 버튼의 attributes 는 컴포넌트 내부에서 만들어져 호출부에 진입점이 없습니다.
  • Flutter 의 이전/다음 버튼이 기본값에서 이름이 없습니다. showLabel 기본값이 false 라 아이콘만 렌더되는데, 그 아이콘은 라벨 없는 SVG 이고 Semantics(label:) 도 붙지 않아 스크린 리더가 "버튼" 이라고만 읽습니다. 같은 버튼이 Web 에서는 aria-label 을 갖습니다. 대안: showLabel: true 로 두면 로케일 문구가 화면 텍스트로 들어가 이름이 생깁니다 — 다만 버튼이 넓어집니다.
  • Flutter 의 말줄임 버튼이 목적지를 말하지 않습니다. 화면 텍스트인 … 만 이름이 되어 "몇 페이지로 이동하는지"가 전달되지 않습니다. Web 은 같은 자리에 paginationJumpToPage(N) 라벨을 붙입니다. 대안: 없습니다 — 말줄임 버튼은 컴포넌트 내부에서 생성됩니다.
  • Flutter 루트에 내비게이션 랜드마크가 없습니다. Web 은 role="navigation" 을 붙이지만 Flutter 루트는 Semantics(container: true, label:) 이라 이름만 있고 역할은 없어, 랜드마크 목록으로 건너뛰는 이동이 되지 않습니다.
  • 화살표 키 이동이 없습니다. 페이지 버튼이 한 줄로 늘어선 컨트롤이라 화살표 이동을 기대하기 쉽지만 두 플랫폼 모두 배선이 없어, 버튼 수만큼 Tab 을 눌러야 합니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명PaginationPagination (동일)
페이지 기준1-based1-based
버튼div 기반 정사각 버튼<div role="button"> 정사각 버튼
생략 표시… 텍스트 (클릭 시 범위 점프)… 텍스트 (클릭 시 범위 점프)
호버MouseRegion + 색 전환CSS :hover 토큰 클래스
가용 폭 측정 레이아웃 중 각 단계를 배치해 들어가는 첫 단계를 그린다 숨긴 측정 사본(inert)을 ResizeObserver 로 재고 단계를 고른다 — 서버 렌더는 첫 단계
  • Table: 페이지네이션이 필요한 대용량 테이블 데이터에 사용
  • Select: 페이지 크기 선택 드롭다운과 조합