식물을 캐릭터화하고, 관리 알림과 일지 기능을 제공하는 모바일 애플리케이션입니다.
1차 배포: 2025.01.02 ~ 2025.04.09 (약 4개월)
Flutter(iOS) 1명, AOS 1명, BE 1명
-
언어:
Dart
-
프레임워크:
Flutter
-
사용한 패키지
# State Management flutter_bloc: ^9.0.0 # Navigation go_router: ^14.6.2 # HTTP Client dio: ^5.8.0+1 # Local Storage flutter_secure_storage: ^9.2.4 shared_preferences: ^2.5.2 # Notification firebase_core: ^3.10.1 firebase_messaging: ^15.2.1 # JSON Serialization freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 # UI/UX cupertino_icons: ^1.0.8 flutter_svg: ^2.0.16 extended_image: ^9.1.0 flutter_screenutil: ^5.9.3 smooth_page_indicator: ^1.2.0+3 contentsize_tabbarview: ^0.0.2 # Utils permission_handler: ^11.4.0 image_picker: ^1.1.2 email_validator: ^3.0.0 intl: ^0.19.0 rxdart: ^0.28.0
회원가입 | 로그인 | 그린룸 추가 |
---|---|---|
![]() |
![]() |
![]() |
그린룸 상세 & 메모 추가 | 프로필 수정 | |
![]() |
![]() |
redirect: (context, state) {
// 딥링크를 통해 전달된 토큰이 있는지 확인
if (state.uri.queryParameters.containsKey('token')) {
final token = state.uri.queryParameters['token'];
return '/sign_up_password?token=$token'; // 리다이렉트
}
return null;
},
routes: [
GoRoute(
path: '/sign_up_password',
builder: (_, state) {
final token = state.uri.queryParameters['token']!;
return SignUpPasswordScreen(token: token);
},
),
],
@override
void initState() {
super.initState();
// SignUpPasswordScreen에서 토큰 검증 요청
context.read<SignUpBloc>().add(
SignUpEvent.emailTokenValidationRequested(widget.token),
);
}
- 모든 API 요청 헤더에 인증 토큰을 추가하고 401 에러 발생 시 Refresh Token으로 Access Token을 자동 갱신
- 갱신된 토큰으로 실패한 요청을 재시도하여 사용자 경험 끊김 방지
- dio_interceptor.dart
- 다크 모드나 테마 전환과 같은 요구사항에도 유연하게 대응할 수 있도록 확장성을 고려
- 확장 메서드를 통해 코드의 가독성과 접근성을 향상
- app_colors_theme.dart
- app_text_theme.dart
// 확장 메서드 적용
extension AppThemeX on ThemeData {
AppColorsTheme get appColors => extension<AppColorsTheme>()!;
AppTextTheme get appTexts => extension<AppTextTheme>()!;
}
// 사용예시
Text(
'그린룸',
style: Theme.of(context).appTexts.body2.copyWith(
color: Theme.of(context).appColors.green300,
),
)
문제 상황
식물 검색 기능에서 사용자가 글자를 입력할 때마다 API가 실시간으로 호출되면서 서버에 과부하가 발생했습니다. 특히 한글 입력 특성상 자음과 모음이 조합되는 과정에서, 예를 들어 "몬스테라" 한 단어를 검색할 때도 9번이나 API 호출이 이루어졌습니다. 이로 인해 불필요한 네트워크 트래픽이 급증하고, 사용자 경험이 크게 저하되는 문제가 발생했습니다.
해결 방법
RxDart
의 debounceTime
연산자를 활용해 사용자가 연속 입력하는 동안 API 호출을 지연시키고, 타이핑이 멈춘 뒤 500밀리초 후에만 실제 검색 요청을 보내도록 구현했습니다. 또한 BLoC 패턴의 이벤트 스트림에 transformer를 적용해 마지막 검색 이벤트만 처리함으로써, 완성되지 않은 중간 검색어로 인한 불필요한 API 호출을 효과적으로 줄일 수 있었습니다.
해결 코드
// AddGreenRoomBloc에서 debounce 적용
on<_SearchPlantsFetched>(
_onSearchPlantsFetched,
transformer: (events, mapper) =>
events.debounceTime(const Duration(milliseconds: 500)).flatMap((event) => mapper(event)),
);
// 검색어 입력 시 이벤트 발생
GRRTextfield(
controller: _searchTEC,
hintText: '식물 이름 또는 품종 검색',
isSearch: true,
onChanged: (text) => context
.read<AddGreenRoomBloc>()
.add(AddGreenRoomPlantEvent.searchPlantsFetched(text)),
),