
Flutter 3.28.0에서 도입될 것으로 예상되는 WidgetPreview 기능은 Flutter 개발자들의 컴포넌트 개발 방식을 획기적으로 변화시킬 것으로 예상됩니다.
본 포스팅에서는 WidgetPreview의 심화 기능과 실제 개발 현장에서의 활용 방안을 상세히 살펴보겠습니다.
@Preview()
List<WidgetPreview> themeVariationPreviews() => [
WidgetPreview(
name: '커스텀 버튼 - 라이트/다크 테마',
child: CustomButton(
onPressed: () {},
label: '결제하기',
),
theme: ThemeData(
colorScheme: ColorScheme.light(
primary: Color(0xFF4A90E2),
secondary: Color(0xFF62B1F6),
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.dark(
primary: Color(0xFF2171CD),
secondary: Color(0xFF3994E7),
),
),
variants: [
PreviewVariant(
name: '비활성화 상태',
builder: (context, child) => CustomButton(
onPressed: null,
label: '결제하기',
),
),
PreviewVariant(
name: '로딩 상태',
builder: (context, child) => CustomButton(
onPressed: () {},
label: '결제하기',
isLoading: true,
),
),
],
),
];
class ResponsiveLayoutPreview extends StatelessWidget {
@Preview()
static List<WidgetPreview> responsivePreview() => [
WidgetPreview(
name: '상품 상세 페이지 - 모바일',
child: ProductDetailPage(),
device: DeviceInfo.genericPhone(
screenSize: Size(360, 640),
pixelRatio: 2.0,
),
),
WidgetPreview(
name: '상품 상세 페이지 - 태블릿',
child: ProductDetailPage(),
device: DeviceInfo.tablet(
screenSize: Size(768, 1024),
pixelRatio: 2.0,
),
),
];
}
class ProductDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return _buildTabletLayout();
}
return _buildMobileLayout();
},
);
}
Widget _buildMobileLayout() {
// 모바일 레이아웃 구현
}
Widget _buildTabletLayout() {
// 태블릿 레이아웃 구현
}
}
@Preview()
List<WidgetPreview> localizationPreviews() => [
WidgetPreview(
name: '주문 확인 다이얼로그 - 한국어',
child: OrderConfirmationDialog(
locale: const Locale('ko'),
orderAmount: 50000,
deliveryDate: DateTime.now().add(Duration(days: 3)),
),
),
WidgetPreview(
name: '주문 확인 다이얼로그 - 영어',
child: OrderConfirmationDialog(
locale: const Locale('en'),
orderAmount: 50000,
deliveryDate: DateTime.now().add(Duration(days: 3)),
),
),
];
class OrderConfirmationDialog extends StatelessWidget {
final Locale locale;
final int orderAmount;
final DateTime deliveryDate;
const OrderConfirmationDialog({
required this.locale,
required this.orderAmount,
required this.deliveryDate,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(locale.languageCode == 'ko' ? '주문 확인' : 'Order Confirmation'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
locale.languageCode == 'ko'
? '주문금액: ${NumberFormat.currency(locale: 'ko_KR', symbol: '₩').format(orderAmount)}'
: 'Order Amount: ${NumberFormat.currency(locale: 'en_US', symbol: '\$').format(orderAmount / 1200)}',
),
const SizedBox(height: 8),
Text(
locale.languageCode == 'ko'
? '배송예정일: ${DateFormat('yyyy년 MM월 dd일').format(deliveryDate)}'
: 'Estimated Delivery: ${DateFormat('MMM dd, yyyy').format(deliveryDate)}',
),
],
),
);
}
}
class AnimatedLoadingButton extends StatefulWidget {
final VoidCallback onPressed;
final String label;
const AnimatedLoadingButton({
required this.onPressed,
required this.label,
});
@override
State<AnimatedLoadingButton> createState() => _AnimatedLoadingButtonState();
}
class _AnimatedLoadingButtonState extends State<AnimatedLoadingButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: ElevatedButton(
onPressed: () {
_controller.forward().then((_) => _controller.reverse());
widget.onPressed();
},
child: Text(widget.label),
),
);
}
}
@Preview()
List<WidgetPreview> animatedButtonPreviews() => [
WidgetPreview(
name: '애니메이션 버튼 - 기본 상태',
child: AnimatedLoadingButton(
onPressed: () {},
label: '클릭하세요',
),
),
];
WidgetPreview는 자동화된 테스트 환경과도 통합될 수 있습니다.
# .github/workflows/widget_preview_test.yml
name: Widget Preview Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
preview_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.28.0'
- name: Run Widget Preview Tests
run: flutter test --tags="preview"
@Preview()
List<WidgetPreview> performancePreview() => [
WidgetPreview(
name: '무한 스크롤 리스트 성능 테스트',
child: InfiniteScrollList(
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
subtitle: Text('Subtitle for item $index'),
leading: CircleAvatar(child: Text('$index')),
),
),
device: DeviceInfo.genericPhone(),
performanceOverlay: true, // 성능 오버레이 활성화
),
];
class InfiniteScrollList extends StatefulWidget {
final Widget Function(BuildContext, int) itemBuilder;
const InfiniteScrollList({required this.itemBuilder});
@override
State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}
class _InfiniteScrollListState extends State<InfiniteScrollList> {
final List<int> _items = List.generate(100, (i) => i);
final ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
_controller.addListener(_onScroll);
}
void _onScroll() {
if (_controller.position.pixels == _controller.position.maxScrollExtent) {
setState(() {
_items.addAll(List.generate(20, (i) => _items.length + i));
});
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: _items.length,
itemBuilder: (context, index) => widget.itemBuilder(context, _items[index]),
);
}
}
WidgetPreview는 단순한 UI 미리보기 도구를 넘어, 컴포넌트 기반 개발의 새로운 패러다임을 제시합니다.
특히 다국어 지원, 반응형 레이아웃, 테마 변경, 애니메이션 등 복잡한 기능들을 효율적으로 테스트하고 관리할 수 있게 해줍니다.