1. WidgetPreview

Flutter 3.28.0에서 도입될 것으로 예상되는 WidgetPreview 기능은 Flutter 개발자들의 컴포넌트 개발 방식을 획기적으로 변화시킬 것으로 예상됩니다.

본 포스팅에서는 WidgetPreview의 심화 기능과 실제 개발 현장에서의 활용 방안을 상세히 살펴보겠습니다.

2. WidgetPreview의 고급 기능 분석

2.1 테마 변형 자동화 테스트

@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,
        ),
      ),
    ],
  ),
];

2.2 반응형 레이아웃 테스트 자동화

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() {
    // 태블릿 레이아웃 구현
  }
}

3. 실전 활용 사례

3.1 다국어 지원 테스트

@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)}',
          ),
        ],
      ),
    );
  }
}

3.2 애니메이션 상태 테스트

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: '클릭하세요',
    ),
  ),
];

4. CI/CD 파이프라인 통합

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"

5. 성능 최적화 테스트

@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]),
    );
  }
}

6. 결론

WidgetPreview는 단순한 UI 미리보기 도구를 넘어, 컴포넌트 기반 개발의 새로운 패러다임을 제시합니다.

특히 다국어 지원, 반응형 레이아웃, 테마 변경, 애니메이션 등 복잡한 기능들을 효율적으로 테스트하고 관리할 수 있게 해줍니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글