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 구현
프로젝트 성과
- 클린 아키텍처 적용으로 유지보수성 향상
- BLoC 패턴으로 상태 관리 체계화
- 성능 최적화로 부드러운 UX 구현
- 반응형 디자인으로 다양한 디바이스 지원
개선 계획
- 단위 테스트 및 위젯 테스트 추가
- 디자인 시스템 구축
- CI/CD 파이프라인 구축
- 접근성 개선
728x90
'코딩 > ♠♠기술 구현' 카테고리의 다른 글
히스토리에서 파이어베이스 데이터 가져오기 (2) | 2024.10.31 |
---|---|
파이어베이스에서 상품정보 가져오기 (0) | 2024.10.31 |
히스토리 화면 구현 과정 (0) | 2024.10.30 |
스크린 유틸 적용과정 (2) | 2024.10.29 |
깃으로 협업하기 (0) | 2024.10.28 |