📌 앱 소개: 생성형 AI를 활용한 개인 맞춤형 레시피 생성 및 공유 플랫폼
🕒 기간: 2025.06.01 ~ 2025.07.04 (1개월)
📱 플랫폼: Flutter 크로스 플랫폼 앱 (iOS, Android)
👥 개발 인원: 3명
💼 역할: AI 레시피 생성, 레시피 수정, 리뷰 관련 기능, 신고 기능, 다국어 지원 개발, 등
🛠️ 주요 사용 기술: Flutter
Dart
Firebase
Riverpod
MVVM
Gemini API
Firestore
Dio
Cloud Functions
Google Cloud Translation API
🔗 GitHub: flutter-fantastic-four/cooki-app
🔗 App Store: apps.apple.com/kr/app/cooki/id6747327839
Gemini AI
기반 멀티모달 레시피 생성 시스템 구축
Gemini 2.0-flash
모델 연동으로 텍스트 입력 및 이미지 인식 기반의 멀티모달 레시피 생성 기능 개발Few-shot
프롬프트 기법과 사용자 선호도(맵기, 아이 친화적 등) 반영 맞춤형 프롬프트 엔지니어링 구현Gemini 1.5-flash
모델을 활용한 별도 입력 검증 시스템 구축JSON Schema
기반 구조화된 응답 형식 정의로 파싱 오류 감소Firestore
와 Firebase Storage
연동을 통한 레시피 및 이미지 저장 기능 구현Flutter Image Compress
를 활용한 이미지 압축과 리사이징을 통해 업로드 시간을 단축하고 스토리지 비용을 최적화했으며, API 토큰 사용량 35% 절감과 생성 속도 향상 달성Firestore
서브컬렉션 구조 활용으로 리뷰 조회 성능 개선Google Cloud Translation API
와 Firebase Cloud Functions
연동을 통한 실시간 번역 기능 구현Flutter l10n
을 활용한 한국어/영어 다국어 UI 구현SharedPreferences
기반 언어 설정 저장 및 언어 설정 페이지로 인한 실시간 언어 변경 기능Flutter Speech-to-Text
플러그인 연동으로 음성 입력 기반 레시피 검색 기능 구현Share Plus
패키지를 활용한 텍스트와 이미지 결합 공유 기능 구현CachedNetworkImage
를 활용한 이미지 캐싱으로 반복 로딩 시간 단축 및 데이터 사용량 절약Shimmer
로딩 애니메이션 구현으로 데이터 로딩 중 인지된 성능 향상 및 사용자 대기 경험 개선PopScope
를 활용한 사용자 데이터 손실 방지 및 확인 다이얼로그 시스템 구현ViewModel
의 try-catch
에서 일괄 포착하여 앱 크래시 방지Enum
에러 키로 변환 후 UI에서 다국어 메시지로 매핑하는 2단계 에러 처리로 기술적 예외와 사용자 메시지 분리ViewModel
단위 테스트 환경 구축 및 관심사 분리 달성MVVM
아키텍처와 Repository
/DataSource
패턴 기반의 계층 구조를 적용해 책임을 분리하고 재사용성 강화Riverpod
을 활용한 전역 상태 관리와 기능별 ViewModel
설계로 상태 흐름을 단순화하고 유지보수 효율 향상Firebase Crashlytics
연동으로 실시간 오류 모니터링 체계 구축DTO
및 Entity
계층 분리로 런타임 오류 방지1. Gemini AI
모델 선택 및 2단계 검증 시스템 구축
요구 사항
사용자가 입력한 텍스트나 이미지로부터 높은 품질의 레시피를 안정적으로 생성하면서, 비음식 관련 입력이나 악의적 프롬프트 조작을 효과적으로 차단해야 함
의사 결정
Gemini 1.5-flash
와 Gemini 2.0-flash
모델을 역할별로 분리한 2단계 검증 시스템 구축을 결정
Gemini 1.5-flash
로 입력 유효성 검사 전담하여 비레시피성 입력, 명령어 조작, 프롬프트 인젝션 시도를 사전 필터링Gemini 2.0-flash
로 실제 레시피 생성 처리하여 최신 모델의 성능과 안정성 확보// 검증 모델 설정
_validationModel = googleAI.generativeModel(
model: 'gemini-1.5-flash',
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
responseSchema: Schema.object(
properties: {'isValid': Schema.boolean()},
),
),
);
// 생성 모델 설정
_recipeGenerationModel = googleAI.generativeModel(
model: 'gemini-2.0-flash',
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
responseSchema: Schema.object(/* 레시피 구조 정의 */),
),
);
2. Firebase Cloud Functions 기반 번역 시스템
요구 사항
실시간 번역 기능이 필요하며, 클라이언트에서 직접 Google Translation API
를 호출하기에는 보안상 API 키 노출 위험이 존재
의사 결정
Firebase Cloud Functions
를 중간 계층으로 활용한 서버리스 번역 시스템 구축을 결정
Google Cloud Translation API
인증 정보를 서버 측에서 안전하게 관리Cloud Functions
레벨에서 통합된 오류 처리 및 클라이언트에 구조화된 응답 반환exports.translateText = onCall({ region: "asia-northeast3" }, async (request) => {
try {
const { text, targetLanguage, sourceLanguage } = request.data;
const translationRequest = {
parent: `projects/${projectId}/locations/global`,
contents: [text],
mimeType: 'text/plain',
targetLanguageCode: targetLanguage,
...(sourceLanguage && { sourceLanguageCode: sourceLanguage }),
};
const [response] = await translationClient.translateText(translationRequest);
return {
success: true,
translatedText: response.translations[0].translatedText,
detectedSourceLanguage: response.translations[0].detectedLanguageCode || sourceLanguage
};
} catch (error) {
throw new Error('Translation failed: ' + error.message);
}
});
3. 통합 로깅 및 크래시 모니터링 유틸리티
요구 사항
협업 환경에서 일관된 에러 처리가 필요하며, 프로덕션 배포 후 사용자 환경의 예외를 개발팀이 신속히 파악·대응할 수 있어야 하고, 로컬 디버깅과 원격 모니터링을 위해 로그 작성 방식을 통일해야 함
의사 결정
Firebase Crashlytics
연동 로깅 유틸리티 개발을 결정
logError()
함수 하나로 통일된 로깅 방식 제공log()
함수로 즉시 확인, 프로덕션에서는 Crashlytics
로 자동 수집runZonedGuarded
를 활용해 Flutter 프레임워크 레벨 예외를 감지하고 로깅하여 앱 크래시를 예방void logError(
dynamic error,
StackTrace stack, {
String? reason,
bool fatal = false,
}) {
final message = reason != null
? '[EXCEPTION] $reason\n$error'
: '[EXCEPTION] $error';
log(message, stackTrace: stack);
FirebaseCrashlytics.instance.recordError(
error,
stack,
reason: reason,
fatal: fatal,
);
}
// 사용 예시
try {
final bytes = await imageDownloadRepository.downloadImage(
recipe.imageUrl!,
);
...
} catch (e, stack) {
logError(e, stack, reason: 'Image download failed');
}
4. 멀티모달 프롬프트 엔지니어링 아키텍처
요구 사항
텍스트 입력, 이미지 입력, 또는 둘의 조합으로 다양한 상황에 대응하는 레시피 생성이 가능해야 하며, 한국어와 영어 사용자 모두에게 일관된 품질의 결과 제공이 필요
의사 결정
템플릿 기반 동적 프롬프트 시스템과 다국어 마크다운 파일 구조 도입을 결정
assets/prompts/ko/
, assets/prompts/en/
구조로 언어별 프롬프트 관리__COOKI_*__
형태의 커스텀 플레이스홀더로 런타임 동적 구성Future<String> _buildRecipePrompt({
String? textInput,
Set<String>? preferences,
required bool hasImage,
required String textOnlyRecipePromptPath,
required String imageRecipePromptPath,
}) async {
if (hasImage) {
String imagePrompt = await rootBundle.loadString(
'assets/prompts/$imageRecipePromptPath',
);
// 동적 섹션 구성
String textContextSection = textInput?.isNotEmpty == true
? await _buildTextContextSection(textInput!)
: '';
String preferencesSection = await _buildPreferencesSection(preferences);
return imagePrompt
.replaceAll(AppConstants.textContextSectionPlaceholder, textContextSection)
.replaceAll(AppConstants.preferencesSectionPlaceholder, preferencesSection);
}
// 텍스트 전용 프롬프트 처리...
}
1. 레시피 생성과 이미지 업로드 병렬 처리 최적화
문제 상황
초기 순차 처리 방식에서 AI 레시피 생성이 끝난 후에야 이미지 업로드를 시작해 전체 소요 시간이 길어지고, 사용자가 오래 대기해야 하는 문제가 발생.
Future.wait()
를 활용하면 두 작업을 동시에 실행해 전체 처리 시간을 줄일 수 있다고 검증._saveRecipe()
메서드에서 이미지 업로드 로직을 _uploadImageIfNeeded()
로 분리.Future.wait()
로 병렬 실행.// 기존 순차 처리 방식
final generatedRecipe = await _generateRecipe(
imageBytes: compressedImageBytes, // Uint8List 바이너리 데이터로 AI에 전송
textInput: state.textInput,
// ...
);
if (generatedRecipe != null) {
final imageUrl = await _uploadImageToStorage(compressedImageBytes, user.id);
final saved = await _saveRecipe(generatedRecipe, user, imageUrl);
}
// 개선된 병렬 처리 방식
final generationTask = _generateRecipe(...);
final imageUploadTask = _uploadImageIfNeeded(...);
final results = await Future.wait([generationTask, imageUploadTask]);
final generated = results[0] as GeneratedRecipe?;
final imageUrl = results[1] as String?;
2. 리뷰 언어 감지 최적화
문제 상황
리뷰 작성 시 언어 감지 API 호출이 동기적으로 처리되어 사용자가 리뷰 저장 완료까지 3초 이상 대기해야 하는 사용성 문제 발생
await
키워드 제거로 언어 감지가 UI 블로킹 없이 별도 스레드에서 처리reviewId
를 미리 확보하여 일관된 처리 플로우 유지// 기존 동기 처리 방식
await saveReview(review);
await detectAndUpdateLanguage(reviewId); // UI 블로킹
// 개선된 비동기 처리 방식
final reviewId = await saveReview(review);
detectAndUpdateLanguage(reviewId); // await 제거로 백그라운드 실행
3. 이미지 리사이징·압축 최적화를 통한 API 비용 절감 및 성능 향상
문제 상황
고해상도 스마트폰 이미지를 그대로 Gemini AI
에 전송할 경우, 해상도 증가로 타일 수가 늘어나 API 토큰 사용량과 비용이 상승하고, 큰 파일 용량으로 인해 업로드 속도 저하와 레시피 생성 지연이 발생함.
maxWidth: 768, maxHeight: 768
으로 리사이징하여 불필요한 타일 생성 방지Flutter Image Compress
로 JPEG 85% 품질 압축, 적절한 이미지 품질을 유지하면서 파일 용량 대폭 축소4. 다중 이미지 병렬 업로드 시 파일명 충돌 문제
Firebase Storage
에서 HTTP 400 오류 발생, 특히 3장 이상 선택 시 빈번Firebase Storage
규칙·할당량 점검, 네트워크 상태 확인 → 모두 정상DateTime.now().millisecondsSinceEpoch
기반이라 병렬 처리 시 동일 값 발생, 압축·업로드 과정 모두에서 중복 가능성 확인millisecondsSinceEpoch
→ microsecondsSinceEpoch
로 변경해 1000배 높은 정밀도 확보