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#
코드 보기
class PaginationDefaultExample extends StatefulComponent {
const PaginationDefaultExample({super.key});
@override
State<PaginationDefaultExample> createState() =>
_PaginationDefaultExampleState();
}
class _PaginationDefaultExampleState extends State<PaginationDefaultExample> {
int _page = 3;
@override
Component build(BuildContext context) {
return Pagination(
page: _page,
totalPages: 10,
onPageChanged: (value) => setState(() => _page = value),
);
}
}
class PaginationDefaultExample extends StatefulWidget {
const PaginationDefaultExample({super.key});
@override
State<PaginationDefaultExample> createState() =>
_PaginationDefaultExampleState();
}
class _PaginationDefaultExampleState extends State<PaginationDefaultExample> {
int _page = 3;
@override
Widget build(BuildContext context) {
return Pagination(
page: _page,
totalPages: 10,
onPageChanged: (p) => setState(() => _page = p),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 테이블, 검색 결과, 상품 목록처럼 많은 데이터를 페이지 단위로 나눠 표시할 때
- 사용자가 특정 페이지로 직접 이동할 수 있어야 할 때
- 전체 데이터 양이 많지 않아 페이지 번호로 탐색이 유용한 경우
대신 다른 컴포넌트를 사용하세요:
RefreshTrigger: 무한 스크롤 방식으로 데이터를 로드할 때Select: 페이지 크기 선택 등 드롭다운 옵션이 필요할 때
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page | int | 필수 | 현재 페이지 (1-based) |
totalPages | int | 필수 | 전체 페이지 수 |
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·labelLarge14 SemiBold. 나머지 숫자는bodyMedium14 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
— 두 플랫폼이 같은 표를 읽는다).
- 이전/다음 라벨 → 아이콘만 (라벨은 접근성 이름으로 남는다)
- 첫/마지막 페이지 바로가기 버튼
- 페이지 번호 창 → 현재 페이지 하나
- 생략(
…) 버튼 → 이전 · 현재 · 다음
폭이 넉넉하면 다시 넓은 단계로 돌아간다. 크롬 값(버튼 크기 · 간격 · 패딩)은 바뀌지 않고, 보이는 버튼의 수만 줄어든다.
애니메이션#
- 페이지 전환 시 별도 애니메이션 없음 (콘텐츠 영역 전환은 부모가 처리)
- 버튼 호버 시 배경색 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)#
역할#
| Flutter | Web | |
|---|---|---|
| 루트 | 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, Webaria-current="page". 현재 페이지 버튼도 계속 활성·포커스 가능하며, 눌러도 같은 인덱스로 콜백이 다시 발생합니다. -
비활성 상태: 첫 페이지의 이전 버튼과 마지막 페이지의 다음 버튼은 Flutter
enabled: false, Webaria-disabled="true"가 됩니다. 컴포넌트 전체가 비활성(enabled: false또는onPageChanged없음)이면 모든 버튼이 같은 표시를 받고(FlutterSemantics(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)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Pagination | Pagination (동일) |
| 페이지 기준 | 1-based | 1-based |
| 버튼 | div 기반 정사각 버튼 | <div role="button"> 정사각 버튼 |
| 생략 표시 | … 텍스트 (클릭 시 범위 점프) | … 텍스트 (클릭 시 범위 점프) |
| 호버 | MouseRegion + 색 전환 | CSS :hover 토큰 클래스 |
| 가용 폭 측정 | 레이아웃 중 각 단계를 배치해 들어가는 첫 단계를 그린다 | 숨긴 측정 사본(inert)을 ResizeObserver 로 재고 단계를 고른다 — 서버 렌더는 첫 단계 |