코딩/♠♠기술 구현

홈화면 UI 만들기

흑백 개발자 2024. 10. 30. 19:57
728x90

프로젝트 개요

소개

  • 올리브영 모바일 앱을 클론 코딩한 프로젝트
  • Flutter와 Firebase를 활용한 크로스 플랫폼 앱 개발
  • BLoC 패턴을 적용한 상태 관리 구현
  • 반응형 디자인으로 다양한 디바이스 지원

기술 스택

  • Frontend: Flutter & Dart
  • Backend: Firebase
  • 상태관리: BLoC Pattern
  • 데이터베이스: Cloud Firestore
  • 인증: Firebase Authentication
  • 스토리지: Firebase Storage

주요 기능 구현

1. 아키텍처 설계

// 프로젝트 구조
lib/
  ├── blocs/          # 상태 관리
  ├── models/         # 데이터 모델
  ├── repositories/   # 데이터 처리
  ├── screens/        # UI 화면
  ├── services/       # 외부 서비스
  ├── utils/          # 유틸리티
  └── widgets/        # 공통 위젯

2. 상태 관리 (BLoC 패턴)

// Event, State, Bloc 구현으로 명확한 데이터 흐름 제어
// home_bloc.dart
class HomeBloc extends Bloc<HomeEvent, HomeState> {
  final HomeRepository repository;
  
  HomeBloc({required this.repository}) : super(HomeInitial()) {
    on<LoadHomeData>(_onLoadHomeData);
    on<RefreshHomeData>(_onRefreshHomeData);
    on<LoadMoreProducts>(_onLoadMoreProducts);
    on<FilterProducts>(_onFilterProducts);
    on<SortProducts>(_onSortProducts);
  }

  Future<void> _onLoadHomeData(LoadHomeData event, Emitter<HomeState> emit) async {
    try {
      emit(HomeLoading());
      // Firebase에서 데이터 로딩
      final banners = await repository.loadBanners();
      final recommended = await repository.loadRecommendedProducts();
      final popular = await repository.loadPopularProducts();
      
      emit(HomeLoaded(
        bannerItems: banners,
        recommendedProducts: recommended,
        popularProducts: popular,
      ));
    } catch (e) {
      emit(HomeError('데이터 로딩 실패: $e'));
    }
  }

  // 페이지네이션 구현
  Future<void> _onLoadMoreProducts(LoadMoreProducts event, Emitter<HomeState> emit) async {
    if (state is HomeLoaded) {
      final currentState = state as HomeLoaded;
      try {
        emit(currentState.copyWith(isLoading: true));
        final moreProducts = await repository.loadMoreProducts(
          lastProduct: currentState.popularProducts.last,
          limit: 10,
        );
        
        emit(currentState.copyWith(
          popularProducts: [...currentState.popularProducts, ...moreProducts],
          isLoading: false,
        ));
      } catch (e) {
        emit(HomeError('추가 데이터 로딩 실패: $e'));
      }
    }
  }
}

3. 반응형 UI 구현

// screen_util.dart
class ScreenUtil {
  static late MediaQueryData _mediaQueryData;
  static late double screenWidth;
  static late double screenHeight;
  static late double defaultSize;
  static late Orientation orientation;

  void init(BuildContext context) {
    _mediaQueryData = MediaQuery.of(context);
    screenWidth = _mediaQueryData.size.width;
    screenHeight = _mediaQueryData.size.height;
    orientation = _mediaQueryData.orientation;
    
    // 기준 사이즈로부터 디바이스 크기에 맞는 비율 계산
    defaultSize = orientation == Orientation.portrait 
        ? screenWidth / 414 
        : screenHeight / 896;
  }

  static double getProportionateScreenHeight(double inputHeight) {
    return (inputHeight / 896.0) * screenHeight;
  }

  static double getProportionateScreenWidth(double inputWidth) {
    return (inputWidth / 414.0) * screenWidth;
  }
}

4. 성능 최적화

// 이미지 캐싱 및 지연 로딩 구현
class OptimizedImageWidget extends StatelessWidget {
  final String imageUrl;
  final double? width;
  final double? height;

  const OptimizedImageWidget({
    required this.imageUrl,
    this.width,
    this.height,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      width: width,
      height: height,
      fit: BoxFit.cover,
      placeholder: (context, url) => ShimmerLoading(
        width: width,
        height: height,
      ),
      errorWidget: (context, url, error) => Icon(Icons.error),
      fadeInDuration: Duration(milliseconds: 300),
      cacheManager: CustomCacheManager.instance,
    );
  }
}

// 커스텀 캐시 매니저
class CustomCacheManager {
  static const key = 'customCacheKey';
  static CacheManager? _instance;
  
  static CacheManager get instance {
    _instance ??= CacheManager(
      Config(
        key,
        stalePeriod: Duration(days: 7),
        maxNrOfCacheObjects: 100,
        repo: JsonCacheInfoRepository(databaseName: key),
        fileService: HttpFileService(),
      ),
    );
    return _instance!;
  }
}

5. 애니메이션 구현

class AnimatedProductCard extends StatefulWidget {
  final Product product;
  final VoidCallback onTap;

  const AnimatedProductCard({
    required this.product,
    required this.onTap,
    Key? key,
  }) : super(key: key);

  @override
  _AnimatedProductCardState createState() => _AnimatedProductCardState();
}

class _AnimatedProductCardState extends State<AnimatedProductCard> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 200),
    );
    
    _scaleAnimation = Tween<double>(
      begin: 1.0,
      end: 0.95,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scaleAnimation,
      builder: (context, child) => Transform.scale(
        scale: _scaleAnimation.value,
        child: GestureDetector(
          onTapDown: (_) => _controller.forward(),
          onTapUp: (_) => _controller.reverse(),
          onTapCancel: () => _controller.reverse(),
          onTap: widget.onTap,
          child: ProductCard(product: widget.product),
        ),
      ),
    );
  }
}

주요 기술적 도전과 해결

1. 상태 관리 최적화

  • 문제: 복잡한 UI 상태와 데이터 흐름 관리의 어려움
  • 해결: BLoC 패턴을 도입하여 비즈니스 로직과 UI를 분리하고 상태 관리를 체계화

2. 성능 최적화

  • 문제: 이미지가 많은 리스트 스크롤 시 성능 저하
  • 해결:
    • 이미지 캐싱 구현
    • 지연 로딩 적용
    • ListView.builder 사용으로 메모리 사용 최적화

3. 반응형 디자인

  • 문제: 다양한 화면 크기와 방향에 대응 필요
  • 해결: ScreenUtil 클래스 구현으로 디바이스 독립적인 UI 구현

프로젝트 성과

  1. 클린 아키텍처 적용으로 유지보수성 향상
  2. BLoC 패턴으로 상태 관리 체계화
  3. 성능 최적화로 부드러운 UX 구현
  4. 반응형 디자인으로 다양한 디바이스 지원

개선 계획

  1. 단위 테스트 및 위젯 테스트 추가
  2. 디자인 시스템 구축
  3. CI/CD 파이프라인 구축
  4. 접근성 개선
728x90