티스토리 뷰
[Flutter] AI 핑계 생성기 '뻥이야' 앱 개발기 - Clean Architecture + Riverpod 116,000줄 규모
권퓨터: Kwonputer 2026. 2. 2. 12:34SUMMARY
뻥이야 (Kxcuse) - AI 핑계 생성 Flutter 앱
MZ세대를 위한 AI 기반 창의적 핑계/변명 생성 모바일 앱 개발기입니다.
Clean Architecture + MVVM + Riverpod 기반 237개 Dart 파일, 약 116,000줄 규모.
GoRouter + KRouter 선언형 라우팅, Hive 4개 DB 차등 라이프사이클 관리.
주요 기술적 도전: AbusePrevention 영구 DB, 자동 토큰 갱신 시스템, 크로스 페이지 데이터 동기화
TABLE OF CONTENTS
목차
왜 이 앱을 만들었나? 아키텍처 설계 주요 기능 기술적 도전과 해결 핵심 코드
WHY
왜 이 앱을 만들었나?
"오늘 왜 늦었어?", "왜 약속에 못 와?" - 일상에서 마주하는 순간마다 재치 있는 핑계가 필요할 때가 있습니다.
단순한 핑계 생성기가 아닌, AI가 상황을 분석하고 캐릭터 페르소나와 말투(Tone)를 반영한 창의적인 핑계를 만들어주는 앱을 기획했습니다.
기술적으로는 Clean Architecture의 레이어 분리, 보안 데이터의 영구 보존, 앱 라이프사이클과 연동된 토큰 관리 등 실서비스 수준의 아키텍처를 구현하고 싶었습니다.
그 결과, 237개 Dart 파일, 약 116,000줄 규모의 Flutter 앱이 탄생했습니다.
ARCHITECTURE
아키텍처 설계
Clean Architecture + MVVM + Riverpod
Presentation Layer — pages(18+), viewmodels(Riverpod ViewModel), widgets(42+), styles(KColors 테마)
Domain Layer — models(불변 엔티티), repositories(인터페이스), usecases(32+ 단일 책임)
Data Layer — repositories(구현체), services(API/Token/Background), dbs(Hive 4개), configs(Dio 인터셉터)
GetIt + Injectable 의존성 주입
코드 생성 기반 DI — build_runner로 DI 코드 자동 생성, 런타임 오류 방지
싱글톤/팩토리 패턴 — Service는 싱글톤, ViewModel은 팩토리로 생명주기 관리
GoRouter + KRouter 네비게이션 시스템
GoRouter 선언형 라우팅 — Scheme enum 기반 타입 안전 라우트, go_transitions 애니메이션
KRouter 래퍼 — 인증 가드(isUseLogin), 다이얼로그 상태 추적, 중복 라우트 방지, Kakao 딥링크 필터링
FEATURES
주요 기능
TokenManagerService - 자동 토큰 갱신
선제적 갱신 — 만료 24시간 전 자동 갱신, 30분 주기 Timer 기반 검증
라이프사이클 연동 — 앱 resume 시 토큰 유효성 즉시 재검증
Dio 인터셉터 — 모든 API 요청에 자동 토큰 주입
Hive 4개 DB - 차등 라이프사이클 관리
UserSettingsDatabase — 토큰, 테마 설정 (로그아웃 시 초기화)
CacheDatabase — API 응답 캐시 (로그아웃 시 초기화)
MemorialDatabase — 핑계 목록 로컬 저장
AbusePreventionDatabase — 가입 보너스/출석 추적 (영구 보존, 탈퇴 시에도 삭제 금지)
BackgroundUpdateService + CRUD Sync Event
백그라운드 동기화 — 비활성 페이지 데이터 자동 갱신 (활성 시 취소)
CRUD Sync Event — BaseViewModel.syncCRUDEvent()로 페이지 간 데이터 일관성
PointProviders — Riverpod 반응형 포인트 상태 전역 관리
KColors 동적 테마 시스템
3가지 테마 모드 — 라이트 / 다크 / 시스템 자동 감지
KColors() 싱글톤 — isDarkTheme 플래그 기반 동적 색상 반환
Pretendard 커스텀 폰트 — 9웨이트 (Thin 100 ~ Black 900)
TROUBLESHOOTING
기술적 도전과 해결
PROBLEM 01
로그아웃/탈퇴 시 보안 데이터와 사용자 데이터의 차등 초기화
로그아웃 시 토큰과 캐시는 삭제해야 하지만, 가입 보너스 악용 방지를 위한 AbusePrevention 데이터는 영구 보존해야 했습니다. 계정 탈퇴 후 재가입해도 보너스를 중복 수령하면 안 됩니다.
문제 상황
단순한 전체 DB clear()로는 보안 데이터까지 삭제되어, 재가입 시 200pt 가입 보너스를 무한 수령할 수 있는 취약점 발생.
SOLUTION - Hive DB 4개 분리 + 차등 초기화 로직
// KRouter.offAllWithLogout() 차등 초기화
Future<void> offAllWithLogout() async {
// 사용자 데이터 초기화 (세션 종료)
await UserSettingsDatabase.clear();
await CacheDatabase.clear();
// AbusePrevention은 절대 clear() 호출 금지!
// 가입 보너스, 출석 체크 등 악용 방지 데이터 영구 보존
clearAllViewModels();
forceCloseAllDialogs();
go(Scheme.login);
}
PROBLEM 02
페이지 간 데이터 불일치 (크로스 페이지 동기화)
핑계 생성 후 목록 페이지로 돌아갔을 때 새 핑계가 표시되지 않거나, 포인트 차감이 다른 페이지에 반영되지 않는 문제가 발생했습니다.
문제 상황
각 페이지의 ViewModel이 독립적으로 데이터를 로드/캐싱하므로, 한 페이지에서의 CRUD 변경이 다른 페이지에 자동 전파되지 않음.
SOLUTION - BaseViewModel CRUD Sync Event 패턴
// BaseViewModel.dart
abstract class BaseViewModel {
// 페이지 간 CRUD 이벤트 동기화
void syncCRUDEvent(CRUDEventType type, dynamic data) {
// 모든 활성 ViewModel에 이벤트 전파
_registeredViewModels.forEach((vm) => vm.onCRUDEvent(type, data));
}
}
// KRouter.back() 호출 시 자동 동기화
Future<void> back() async {
await syncWithMemorialDb(); // 로컬 DB 동기화
_notifyActiveViewModels(); // ViewModel 갱신 트리거
GoRouter.of(context).pop();
}
PROBLEM 03
토큰 만료로 인한 갑작스러운 로그아웃
사용자가 앱을 사용하던 중 토큰이 만료되어 갑자기 로그인 화면으로 이동하는 UX 문제가 발생했습니다. 특히 백그라운드에서 앱이 오래 있다가 포그라운드로 돌아올 때 자주 발생했습니다.
SOLUTION - TokenManagerService 선제적 갱신 + 라이프사이클 연동
// TokenManagerService.dart
class TokenManagerService {
Timer? _refreshTimer;
void startAutoRefresh() {
// 30분마다 토큰 상태 검증
_refreshTimer = Timer.periodic(Duration(minutes: 30), (_) {
_checkAndRefreshIfNeeded();
});
}
Future<void> _checkAndRefreshIfNeeded() async {
final expiresAt = await _db.getTokenExpiry();
final remaining = expiresAt.difference(DateTime.now());
// 만료 24시간 전에 선제적 갱신
if (remaining < Duration(hours: 24)) {
await refreshToken();
}
}
}
// main.dart - 앱 라이프사이클 훅
WidgetsBinding.instance.addObserver(
LifecycleEventHandler(
onResume: () => TokenManagerService.validateAndRefresh(),
),
);
CORE CODE
핵심 코드
1. KRouter 네비게이션 래퍼
// KRouter.dart - 인증 가드 + 다이얼로그 관리
class KRouter {
static final Set<Dialog> _activeDialogs = {};
static Future<void> to(Scheme scheme, {Object? extra}) async {
// 인증 필요 라우트 체크
if (scheme.isUseLogin && !await _isLoggedIn()) {
return go(Scheme.login);
}
// Kakao OAuth 딥링크 필터링
if (_isKakaoDeepLink(scheme)) return;
// 중복 라우트 방지
if (_currentRoute == scheme.path) return;
GoRouter.of(_context).push(scheme.path, extra: extra);
}
static void forceCloseAllDialogs() {
for (final dialog in _activeDialogs.toList()) {
Navigator.of(_context).pop();
}
_activeDialogs.clear();
}
}
2. Riverpod ViewModel Provider
// providers/excuse_providers.dart
@riverpod
class ExcuseListViewModel extends _$ExcuseListViewModel {
@override
AsyncValue<List<Excuse>> build() => const AsyncValue.loading();
Future<void> loadExcuses() async {
state = const AsyncValue.loading();
try {
final useCase = ref.read(getExcuseListUseCaseProvider);
final excuses = await useCase.execute();
state = AsyncValue.data(excuses);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
// CRUD Sync Event 수신
void onCRUDEvent(CRUDEventType type, Excuse excuse) {
if (type == CRUDEventType.created) {
state = state.whenData((list) => [excuse, ...list]);
}
}
}
3. KColors 동적 테마
// styles/KColors.dart
class KColors {
static final KColors _instance = KColors._();
factory KColors() => _instance;
KColors._();
bool isDarkTheme = false;
Color get primary => isDarkTheme ? Color(0xFF667EEA) : Color(0xFF5A67D8);
Color get background => isDarkTheme ? Color(0xFF1A1A2E) : Color(0xFFF7FAFC);
Color get textPrimary => isDarkTheme ? Color(0xFFE2E8F0) : Color(0xFF2D3748);
void updateTheme(AppThemeTypes type) {
switch (type) {
case AppThemeTypes.light: isDarkTheme = false;
case AppThemeTypes.dark: isDarkTheme = true;
case AppThemeTypes.system:
isDarkTheme = WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark;
}
}
}
TECH STACK
기술 스택
Core Framework
Flutter 3.32.1 Dart SDK ^3.5.4
State Management & DI
flutter_riverpod 2.6.1 riverpod_annotation 2.6.1 get_it 8.0.3 injectable 2.5.0
Routing & Navigation
go_router 15.1.2 go_transitions 0.8.1
Local Storage & Network
hive 2.2.3 dio 5.8.0
Authentication
kakao_flutter_sdk 1.9.7 sign_in_with_apple 7.0.1
Firebase & Analytics
firebase_core 3.4.0 firebase_crashlytics 4.3.6 firebase_remote_config
Animation & UI
flutter_animate 4.5.2 lottie 3.2.0 shimmer 3.0.0 Pretendard 9 weights
CONCLUSION
결론
뻥이야는 단순한 핑계 생성 앱을 넘어, 실서비스 수준의 아키텍처를 구현한 Flutter 프로젝트입니다.
Clean Architecture의 엄격한 레이어 분리, Hive DB의 차등 라이프사이클 관리, TokenManagerService의 선제적 토큰 갱신, KRouter의 인증 가드와 다이얼로그 관리 등 실무에서 마주하는 복잡한 요구사항들을 해결했습니다.
특히 AbusePrevention 영구 DB를 통한 보안 데이터 보호와 CRUD Sync Event 패턴을 통한 크로스 페이지 데이터 일관성은 이 프로젝트의 핵심 설계입니다.
237개 Dart 파일, 약 116,000줄 규모의 이 프로젝트를 통해 Flutter 앱 개발의 전반적인 아키텍처 설계 역량을 쌓을 수 있었습니다.
긴 글을 읽어주셔서 감사합니다!
처음 이 앱을 기획했을 때는 "재미있는 핑계 생성기"를 만들고 싶었습니다.
하지만 개발을 진행하면서 토큰 만료 문제, 보안 데이터 보호, 페이지 간 데이터 동기화 등 예상치 못한 도전들을 마주하게 되었습니다.
특히 "로그아웃해도 삭제되면 안 되는 데이터"와 "삭제되어야 하는 데이터"를 어떻게 분리할 것인가에 대한 고민은 Hive 4개 DB 차등 라이프사이클 관리라는 해결책으로 이어졌습니다.
이 프로젝트가 Flutter로 실서비스를 개발하시는 분들께 작은 도움이 되었으면 합니다.
다음 포스트에서는 뻥이야의 AWS Serverless 백엔드 (Lambda + DynamoDB + OpenAI GPT)에 대해 다루겠습니다!
'Flutter Project > 뻥이야' 카테고리의 다른 글
| [AWS] AI 핑계 생성 서비스 서버리스 백엔드 - CDK v2 + Lambda + DynamoDB 6 GSI + GPT (0) | 2026.02.02 |
|---|
- Total
- Today
- Yesterday
- flutter
- OpenAI GPT
- 파이썬
- Clean Architecture
- Single Table Design
- KE-T5
- injectable
- TypeScript
- 서버리스 아키텍처
- Prompt Engineering
- python
- 개발자
- 상태관리
- 파이썬 기초
- ai 게임 개발
- kotlin
- https://www.kwonputer.shop/
- Compose
- https://github.com/kwongeneral/kortfolio.git
- 내러티브 게임
- flutter 개발자
- aws lambda
- dynamodb
- flutter 면접 질문
- 클린 아키텍처
- python 기초
- AWS CDK
- riverpod
- 자막 생성기
- 크로스플랫폼
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |













