Flutter는 본래 모바일 앱 개발을 위해 태어난 프레임워크이지만, 이제는 웹(Web)과 데스크톱(Desktop)까지 아우르는 강력한 크로스플랫폼 솔루션으로 자리 잡았습니다. 특히 Flutter Web은 단일 코드베이스로 모바일과 웹을 동시에 지원할 수 있다는 점에서 많은 개발자에게 매력적인 선택지가 되고 있습니다.
하지만 Flutter의 근간이 모바일에 있다 보니, 웹 개발에서 당연하게 여겨지는 몇몇 UX/UI 패턴들은 기본적으로 제공되지 않는 경우가 있습니다. 대표적인 예가 바로 '숫자 기반 페이징(Numbered Pagination)'입니다. 모바일에서는 보통 무한 스크롤(Infinite Scroll) 방식을 통해 아래로 스크롤하면 새로운 콘텐츠가 로딩되는 방식을 선호하지만, 관리자 페이지, 게시판, 검색 결과 등 대량의 데이터를 정제된 형태로 보여줘야 하는 웹 환경에서는 전통적인 페이지 번호 방식이 훨씬 직관적이고 효율적입니다.
사용자는 페이지 번호를 통해 전체 데이터의 양을 가늠할 수 있고, 원하는 페이지로 즉시 점프할 수 있으며, 특정 페이지를 북마크하거나 다른 사람에게 공유하기도 용이합니다. 이 글에서는 Flutter Web 프로젝트에서 사용자들이 가장 익숙하게 느끼는 숫자 페이징 UI를 A부터 Z까지, 완벽하게 구현하는 방법을 상세하게 다뤄보겠습니다. 단순히 코드를 복사-붙여넣기 하는 수준을 넘어, 페이징의 핵심 원리를 이해하고, 재사용 가능한 위젯으로 모듈화하며, 실제 프로젝트에 적용할 수 있는 수준의 실용적인 팁까지 모두 담았습니다.
페이징의 핵심 원리: 무엇을 알아야 할까?
멋진 UI를 가진 페이징 위젯을 만들기 전에, 페이징 시스템을 구동하는 데 필요한 핵심 데이터와 로직을 이해해야 합니다. 어떤 형태의 페이징이든 아래의 네 가지 기본 요소를 기반으로 동작합니다.
totalItems
(총 아이템 개수): 데이터베이스에 저장된 전체 게시물, 상품, 회원 등의 총 개수입니다. 이 값은 백엔드 API를 통해 받아오는 것이 일반적입니다. 예를 들어, 총 1,234개의 게시물이 있다면totalItems
는 1234가 됩니다.itemsPerPage
(페이지 당 아이템 개수): 한 페이지에 보여줄 아이템의 개수를 의미합니다. '목록 10개씩 보기', '50개씩 보기'와 같이 사용자가 직접 선택하게 할 수도 있습니다. 이 글에서는 10개로 고정하여 진행하겠습니다.currentPage
(현재 페이지 번호): 사용자가 현재 보고 있는 페이지의 번호입니다. 이 값은 1부터 시작하며, 사용자가 페이지 번호를 클릭할 때마다 변경됩니다.totalPages
(총 페이지 개수): 위 세 가지 값을 통해 계산되는 파생 값입니다. 전체 아이템 개수를 페이지 당 아이템 개수로 나눈 후, 소수점이 나오면 올림 처리를 해야 합니다. 예를 들어,totalItems
가 123개이고itemsPerPage
가 10이라면, 총 12.3 페이지가 필요하므로 올림하여totalPages
는 13이 됩니다.
(totalItems / itemsPerPage).ceil()
이 네 가지 데이터만 있으면 우리는 어떤 페이지에 어떤 데이터를 보여줘야 할지, 그리고 페이징 UI를 어떻게 그려야 할지 모두 계산할 수 있습니다. 예를 들어 currentPage
가 3이고 itemsPerPage
가 10이라면, 우리는 백엔드에 "31번째부터 40번째까지의 데이터를 주세요"라고 요청할 수 있습니다.
우리가 만들 페이징 위젯은 바로 이 `currentPage`를 사용자와 상호작용하며 변경하고, 부모 위젯에게 변경된 페이지 번호를 알려주는 역할을 담당하게 될 것입니다.
프로젝트 준비 및 기본 구조 설정하기
이제 본격적으로 코드를 작성해 보겠습니다. 먼저 Flutter 프로젝트를 생성하고 웹 지원을 활성화합니다. 터미널에서 다음 명령어를 실행하세요.
# Flutter 프로젝트 생성
flutter create flutter_web_pagination
# 생성된 프로젝트 디렉토리로 이동
cd flutter_web_pagination
# (이미 활성화되어 있다면 생략 가능)
flutter config --enable-web
# 프로젝트 실행 (Chrome에서)
flutter run -d chrome
프로젝트가 준비되었다면, 실제 데이터를 연동하기 전에 가상의 데이터를 만들어 페이징 로직을 테스트하는 것이 효율적입니다. lib/main.dart
파일을 열고 전체 코드를 아래와 같이 수정하여 기본 틀을 잡아줍니다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Web Pagination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 상태 변수 선언
late List<String> _dummyData; // 전체 더미 데이터
int _currentPage = 1;
final int _itemsPerPage = 10;
List<String> _pagedData = []; // 현재 페이지에 보여질 데이터
@override
void initState() {
super.initState();
// 153개의 가상 데이터를 생성
_dummyData = List.generate(153, (index) => 'Item ${index + 1}');
_fetchPage(_currentPage);
}
// 특정 페이지의 데이터를 가져오는 함수
void _fetchPage(int page) {
setState(() {
_currentPage = page;
int startIndex = (page - 1) * _itemsPerPage;
int endIndex = startIndex + _itemsPerPage;
if (endIndex > _dummyData.length) {
endIndex = _dummyData.length;
}
_pagedData = _dummyData.sublist(startIndex, endIndex);
});
}
int get _totalPages => (_dummyData.length / _itemsPerPage).ceil();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Web Pagination Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 데이터 목록을 보여줄 부분
Expanded(
child: ListView.builder(
itemCount: _pagedData.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_pagedData[index]),
leading: CircleAvatar(child: Text(((_currentPage - 1) * _itemsPerPage + index + 1).toString())),
);
},
),
),
const Divider(),
// 페이징 컨트롤 위젯이 들어갈 부분 (아직 비어있음)
Container(
height: 50,
alignment: Alignment.center,
child: const Text('Pagination controls will be here.'),
),
],
),
),
);
}
}
위 코드는 페이징을 위한 기본 환경을 설정합니다. _dummyData
에 153개의 아이템을 생성하고, _fetchPage
함수는 요청된 페이지 번호에 맞는 데이터를 잘라내어 _pagedData
를 업데이트합니다. 이제 화면 하단에 실제 페이징 컨트롤 위젯을 만들어 채워 넣을 차례입니다.
재사용 가능한 페이징 위젯(PaginationControls) 만들기
페이징 UI는 여러 페이지에서 재사용될 가능성이 높으므로, 별도의 위젯으로 분리하는 것이 좋습니다. lib
폴더에 pagination_controls.dart
파일을 새로 만들고, 아래 내용을 작성해 봅시다. 이 위젯은 상태를 직접 관리하지 않고, 부모로부터 필요한 데이터(currentPage
, totalPages
)를 받고, 페이지 변경이 발생하면 콜백 함수(onPageChanged
)를 호출하는 방식으로 설계하여 재사용성을 극대화합니다.
1단계: 위젯의 기본 구조 정의
StatelessWidget
으로 위젯의 뼈대를 만듭니다. 필요한 파라미터들을 정의하고 생성자를 작성합니다.
// lib/pagination_controls.dart
import 'package:flutter/material.dart';
class PaginationControls extends StatelessWidget {
/// 총 페이지 수
final int totalPages;
/// 현재 페이지 번호 (1부터 시작)
final int currentPage;
/// 페이지 변경 시 호출되는 콜백 함수
final Function(int page) onPageChanged;
/// 한 번에 보여줄 페이지 번호의 개수
final int pagesToShow;
const PaginationControls({
super.key,
required this.totalPages,
required this.currentPage,
required this.onPageChanged,
this.pagesToShow = 5, // 기본값은 5개
});
@override
Widget build(BuildContext context) {
// 위젯의 UI는 여기에 구현됩니다.
// 지금은 비어있는 Row를 반환합니다.
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [],
);
}
}
2단계: 페이지 번호 목록 생성 로직 구현
페이징 위젯의 가장 핵심적인 부분입니다. 현재 페이지를 중심으로 `pagesToShow` 개수만큼의 페이지 번호 목록을 동적으로 생성해야 합니다. 예를 들어, 총 페이지가 30개이고, 한 번에 5개의 페이지 번호를 보여주며, 현재 페이지가 15일 경우, [13, 14, 15, 16, 17]과 같은 목록을 만들어내야 합니다. 이 로직을 별도의 함수로 분리하면 코드가 깔끔해집니다.
// PaginationControls 위젯 내부에 추가할 함수
List<int> _getPageNumbers() {
if (totalPages <= pagesToShow) {
// 총 페이지 수가 보여줄 페이지 수보다 작거나 같으면 모든 페이지 번호를 반환
return List.generate(totalPages, (index) => index + 1);
}
List<int> pageNumbers = [];
int half = pagesToShow ~/ 2; // 보여줄 페이지 수의 절반
int startPage;
int endPage;
if (currentPage <= half) {
// 현재 페이지가 앞부분에 있을 경우
startPage = 1;
endPage = pagesToShow;
} else if (currentPage + half >= totalPages) {
// 현재 페이지가 뒷부분에 있을 경우
startPage = totalPages - pagesToShow + 1;
endPage = totalPages;
} else {
// 현재 페이지가 중간에 있을 경우
startPage = currentPage - half;
endPage = currentPage + half;
}
for (int i = startPage; i <= endPage; i++) {
pageNumbers.add(i);
}
return pageNumbers;
}
이 `_getPageNumbers` 함수는 현재 페이지(`currentPage`)와 총 페이지 수(`totalPages`)를 고려하여 사용자에게 보여줄 페이지 번호의 범위를 지능적으로 계산합니다.
3단계: UI 요소(버튼) 빌드 및 스타일링
이제 `_getPageNumbers` 함수로 생성된 번호 목록과 '처음', '이전', '다음', '마지막' 버튼을 실제 UI로 그려낼 차례입니다. `build` 메소드를 아래와 같이 완성합니다.

최종적으로 구현될 페이징 컨트롤의 모습
// PaginationControls 위젯의 build 메소드 전체 코드
@override
Widget build(BuildContext context) {
final pageNumbers = _getPageNumbers();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// '처음' 버튼
_buildIconButton(
icon: Icons.first_page,
onPressed: currentPage > 1 ? () => onPageChanged(1) : null,
),
// '이전' 버튼
_buildIconButton(
icon: Icons.chevron_left,
onPressed: currentPage > 1 ? () => onPageChanged(currentPage - 1) : null,
),
const SizedBox(width: 8),
// 페이지 번호 버튼들
...pageNumbers.map((pageNumber) {
return _buildPageButton(pageNumber);
}).toList(),
const SizedBox(width: 8),
// '다음' 버튼
_buildIconButton(
icon: Icons.chevron_right,
onPressed: currentPage < totalPages ? () => onPageChanged(currentPage + 1) : null,
),
// '마지막' 버튼
_buildIconButton(
icon: Icons.last_page,
onPressed: currentPage < totalPages ? () => onPageChanged(totalPages) : null,
),
],
);
}
// 아이콘 버튼을 만드는 헬퍼 메소드
Widget _buildIconButton({required IconData icon, required VoidCallback? onPressed}) {
return IconButton(
icon: Icon(icon),
onPressed: onPressed,
splashRadius: 20.0,
color: onPressed == null ? Colors.grey : Colors.black,
);
}
// 페이지 번호 버튼을 만드는 헬퍼 메소드
Widget _buildPageButton(int pageNumber) {
bool isCurrentPage = currentPage == pageNumber;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: TextButton(
onPressed: () => onPageChanged(pageNumber),
style: TextButton.styleFrom(
minimumSize: const Size(40, 40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: isCurrentPage ? Colors.blue : Colors.transparent,
foregroundColor: isCurrentPage ? Colors.white : Colors.black,
),
child: Text('$pageNumber'),
),
);
}
이 코드는 `_getPageNumbers`가 반환한 리스트를 `map`을 이용해 각각의 `_buildPageButton`으로 변환합니다. `_buildPageButton` 에서는 현재 페이지인지 여부(`isCurrentPage`)를 확인하여 배경색과 글자색을 다르게 적용함으로써 활성화된 페이지를 시각적으로 강조합니다. 또한 '처음', '이전', '다음', '마지막' 버튼은 현재 페이지의 위치에 따라 비활성화(onPressed가 null) 처리하여 사용자가 불필요한 클릭을 하지 않도록 방지합니다.
메인 페이지와 페이징 위젯 연동하기
이제 완성된 `PaginationControls` 위젯을 `main.dart`의 `MyHomePage`에 통합할 차례입니다. 아까 'Pagination controls will be here.' 라고 표시해 둔 부분을 `PaginationControls` 위젯으로 교체하고, 필요한 값들을 넘겨주면 됩니다.
먼저 `main.dart` 파일 상단에 `pagination_controls.dart`를 import 합니다.
import 'package.flutter/material.dart';
import 'pagination_controls.dart'; // 방금 만든 위젯 import
// ... main, MyApp 코드 생략 ...
그리고 `_MyHomePageState`의 `build` 메소드에서 `Container` 부분을 아래와 같이 수정합니다.
// ... _MyHomePageState 내부 ...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Web Pagination Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 데이터 목록을 보여줄 부분 (이전과 동일)
Expanded(
child: ListView.builder(
itemCount: _pagedData.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_pagedData[index]),
leading: CircleAvatar(child: Text(((_currentPage - 1) * _itemsPerPage + index + 1).toString())),
);
},
),
),
const Divider(),
// 페이징 컨트롤 위젯으로 교체!
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PaginationControls(
totalPages: _totalPages,
currentPage: _currentPage,
onPageChanged: (page) {
// 페이지 번호가 클릭되었을 때 _fetchPage 함수를 호출하여
// 상태를 업데이트하고 화면을 다시 그립니다.
_fetchPage(page);
},
),
),
],
),
),
);
}
이제 모든 조각이 맞춰졌습니다. `PaginationControls`는 현재 페이지(`_currentPage`)와 총 페이지(`_totalPages`)를 props로 전달받아 화면을 그립니다. 사용자가 페이지 번호를 클릭하면, `onPageChanged` 콜백이 실행되고, 이 콜백은 `_MyHomePageState`의 `_fetchPage` 함수를 호출합니다. `_fetchPage` 함수는 `setState`를 통해 `_currentPage`를 업데이트하고, 새로운 페이지에 맞는 데이터를 `_pagedData`에 담아 화면을 갱신합니다. 이로써 완벽하게 동작하는 페이징 시스템이 완성되었습니다!
전체 코드 및 실행 결과
지금까지 작성한 전체 코드는 다음과 같습니다. `main.dart`와 `pagination_controls.dart` 두 개의 파일로 구성됩니다.
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_web_pagination/pagination_controls.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Web Pagination Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.indigo,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late List<String> _dummyData;
int _currentPage = 1;
final int _itemsPerPage = 10;
List<String> _pagedData = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_dummyData = List.generate(153, (index) => 'Sample Data Entry No. ${index + 1}');
_fetchPage(_currentPage);
}
Future<void> _fetchPage(int page) async {
setState(() {
_isLoading = true;
});
// 실제 API 호출을 시뮬레이션하기 위한 지연
await Future.delayed(const Duration(milliseconds: 300));
setState(() {
_currentPage = page;
int startIndex = (page - 1) * _itemsPerPage;
int endIndex = startIndex + _itemsPerPage;
if (endIndex > _dummyData.length) {
endIndex = _dummyData.length;
}
_pagedData = _dummyData.sublist(startIndex, endIndex);
_isLoading = false;
});
}
int get _totalPages => (_dummyData.length / _itemsPerPage).ceil();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Web - Numbered Pagination'),
elevation: 0,
),
body: Column(
children: <Widget>[
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _pagedData.length,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
title: Text(_pagedData[index]),
subtitle: Text('This is item #${(_currentPage - 1) * _itemsPerPage + index + 1} from the full list.'),
leading: CircleAvatar(
child: Text(
((_currentPage - 1) * _itemsPerPage + index + 1).toString(),
style: const TextStyle(fontSize: 12),
),
),
),
);
},
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: PaginationControls(
totalPages: _totalPages,
currentPage: _currentPage,
onPageChanged: (page) {
_fetchPage(page);
},
),
),
],
),
);
}
}
lib/pagination_controls.dart
import 'package:flutter/material.dart';
import 'dart:math';
class PaginationControls extends StatelessWidget {
final int totalPages;
final int currentPage;
final Function(int page) onPageChanged;
final int pagesToShow;
const PaginationControls({
super.key,
required this.totalPages,
required this.currentPage,
required this.onPageChanged,
this.pagesToShow = 5,
});
List<int> _getPageNumbers() {
if (totalPages <= 0) return [];
if (totalPages <= pagesToShow) {
return List.generate(totalPages, (index) => index + 1);
}
List<int> pageNumbers = [];
int half = pagesToShow ~/ 2;
int startPage = max(1, currentPage - half);
int endPage = min(totalPages, currentPage + half);
if (currentPage - half < 1) {
endPage = pagesToShow;
}
if (currentPage + half > totalPages) {
startPage = totalPages - pagesToShow + 1;
}
for (int i = startPage; i <= endPage; i++) {
pageNumbers.add(i);
}
return pageNumbers;
}
@override
Widget build(BuildContext context) {
if (totalPages <= 1) return const SizedBox.shrink(); // 페이지가 하나 이하면 컨트롤을 숨김
final pageNumbers = _getPageNumbers();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIconButton(
context: context,
icon: Icons.first_page,
tooltip: 'First Page',
onPressed: currentPage > 1 ? () => onPageChanged(1) : null,
),
_buildIconButton(
context: context,
icon: Icons.chevron_left,
tooltip: 'Previous Page',
onPressed: currentPage > 1 ? () => onPageChanged(currentPage - 1) : null,
),
const SizedBox(width: 8),
...pageNumbers.map((pageNumber) {
return _buildPageButton(context, pageNumber);
}).toList(),
const SizedBox(width: 8),
_buildIconButton(
context: context,
icon: Icons.chevron_right,
tooltip: 'Next Page',
onPressed: currentPage < totalPages ? () => onPageChanged(currentPage + 1) : null,
),
_buildIconButton(
context: context,
icon: Icons.last_page,
tooltip: 'Last Page',
onPressed: currentPage < totalPages ? () => onPageChanged(totalPages) : null,
),
],
);
}
Widget _buildIconButton({
required BuildContext context,
required IconData icon,
required String tooltip,
required VoidCallback? onPressed,
}) {
return Tooltip(
message: tooltip,
child: IconButton(
icon: Icon(icon),
onPressed: onPressed,
splashRadius: 20.0,
color: onPressed == null ? Theme.of(context).disabledColor : Theme.of(context).textTheme.bodyLarge?.color,
),
);
}
Widget _buildPageButton(BuildContext context, int pageNumber) {
bool isCurrentPage = currentPage == pageNumber;
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: TextButton(
onPressed: () => onPageChanged(pageNumber),
style: TextButton.styleFrom(
minimumSize: const Size(42, 42),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: isCurrentPage ? theme.primaryColor : Colors.grey.shade300,
width: 1,
),
),
backgroundColor: isCurrentPage ? theme.primaryColor : theme.colorScheme.surface,
),
child: Text(
'$pageNumber',
style: TextStyle(
fontWeight: isCurrentPage ? FontWeight.bold : FontWeight.normal,
color: isCurrentPage ? theme.colorScheme.onPrimary : theme.textTheme.bodyLarge?.color,
),
),
),
);
}
}
위 코드를 실행하면, 로딩 인디케이터가 잠시 나타난 후 10개의 리스트 아이템과 하단에 완벽하게 동작하는 페이징 컨트롤이 나타나는 것을 확인할 수 있습니다. 각 버튼을 클릭하며 페이지가 부드럽게 전환되는 것을 테스트해보세요.
고급 기능: 반응형 UI 및 '...' 생략 부호 추가
지금 만든 페이징 컨트롤은 훌륭하게 동작하지만, 실제 프로덕션 환경에서는 몇 가지 추가적인 고려사항이 있습니다.
1. 반응형 UI (Responsive UI)
웹은 다양한 화면 크기를 가집니다. 스마트폰의 작은 화면에서는 5개의 페이지 번호를 보여주기 버거울 수 있습니다. `LayoutBuilder`나 `MediaQuery`를 사용하여 화면 너비에 따라 `pagesToShow` 값을 동적으로 변경할 수 있습니다.
// MyHomePage의 build 메소드 내에서
final double screenWidth = MediaQuery.of(context).size.width;
final int pagesToShow = screenWidth < 600 ? 3 : 5; // 화면이 600px보다 작으면 3개, 크면 5개
// ... PaginationControls 위젯에 pagesToShow 값을 전달 ...
PaginationControls(
// ... 다른 파라미터들
pagesToShow: pagesToShow,
)
2. '...' 생략 부호 (Ellipsis)
페이지 수가 매우 많을 때(예: 1000 페이지), 사용자가 중간 페이지로 빠르게 이동할 수 있도록 '...' 버튼을 추가하는 것이 일반적인 UX 패턴입니다. 예를 들어, `1 2 ... 50 51 52 ... 999 1000` 과 같은 형태입니다. 이는 `_getPageNumbers` 로직을 더욱 정교하게 만들어 구현할 수 있습니다.
이를 구현하려면 `_getPageNumbers` 함수를 수정하여 특정 조건에서 페이지 번호 대신 특수 값(예: 0 또는 -1)을 리스트에 추가하고, `build` 메소드에서는 이 특수 값을 만나면 '...' 텍스트를 표시하도록 분기 처리를 해야 합니다. 이는 상당한 로직 변경을 요구하지만, 사용자 경험을 한 단계 더 끌어올릴 수 있는 중요한 개선점입니다.
마치며
이 글에서는 Flutter Web에서 숫자 기반 페이징 UI를 처음부터 끝까지 구현하는 전 과정을 살펴보았습니다. 페이징의 핵심 원리를 이해하고, 재사용 가능한 위젯으로 기능을 분리했으며, 실제 애플리케이션에 통합하는 방법까지 다루었습니다. 여기서 제시한 코드는 여러분의 프로젝트에 바로 적용하거나, 필요에 맞게 커스터마이징할 수 있는 훌륭한 시작점이 될 것입니다.
Flutter가 비록 모바일 우선 프레임워크일지라도, 그 유연성과 확장성은 웹에서도 데스크톱 수준의 풍부하고 직관적인 사용자 경험을 만들어낼 수 있는 무한한 가능성을 품고 있습니다. 오늘 배운 내용을 바탕으로 여러분의 Flutter Web 프로젝트에 멋진 페이징 기능을 추가하여 사용자의 편의성을 높여보시길 바랍니다.
감사합니다. 덕분에 쉽게 페이징처리를 할수 있었네요.
ReplyDelete