📌 앱 소개: 생성형 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배 높은 정밀도 확보