Flutter 앱에 온디바이스 AI 넣기 — Gemma 4 구현 삽질기
Flutter 앱에 온디바이스 AI 넣기 — Gemma 4 구현 삽질기
ttapp에 온디바이스 AI 기능을 추가했다. 인터넷 없이 기기 안에서 AI와 대화할 수 있는 기능이다. Google의 Gemma 4 모델을 flutter_gemma 패키지로 연동했고, 구현 과정에서 꽤 많은 삽질을 했다. 이 글은 그 과정의 기록이다.
왜 온디바이스 AI인가?
서버 AI(Claude, GPT 등)는 훌륭하지만 몇 가지 한계가 있다.
- 인터넷 필수: 오프라인 환경에서는 사용 불가
- 비용: API 호출마다 요금 발생
- 개인정보: 입력 데이터가 서버로 전송됨
온디바이스 AI는 이 문제를 전부 해결한다. 기기 안에서만 돌아가니까.
기술 스택
- Flutter + flutter_gemma 0.13.0
- Gemma 4 (E2B: ~2.6GB, E4B: ~3.7GB)
- HuggingFace litert-community 공개 레포 (인증 불필요)
- ModelFileType.litertlm (LiteRT-LM 포맷)
삽질 1: 모델 다운로드가 0%에서 멈춘다
처음에 다운로드를 구현하고 테스트하니 0%에서 영원히 멈췄다. 에러 메시지도 없었다. 30분을 기다려봤지만 아무 일도 없었다.
원인은 황당하게도 main()에 초기화 한 줄이 빠진 거였다.
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ★ 이게 없으면 다운로드가 0%에서 멈춤
await FlutterGemma.initialize();
runApp(const MyApp());
}
공식 문서에는 있는 내용인데 놓쳤다. 패키지 내부에서 이 초기화가 없으면 에러를 던지는 대신 그냥 조용히 멈춰버린다. 디버깅하기 가장 어려운 종류의 버그다.
삽질 2: Play Store 리젝 — FOREGROUND_SERVICE
처음엔 foreground: true로 다운로드를 설정했다. 3GB짜리 파일이니까 포그라운드 서비스를 쓰는 게 맞다고 생각했다.
Play Store 심사에서 리젝이 왔다.
"귀하의 앱이 FOREGROUND_SERVICE 권한을 사용하는지 알려주세요."
flutter_gemma의 foreground 다운로드는 Android FOREGROUND_SERVICE 권한을 요구한다. 그런데 이 권한은 Play Store에서 사용 목적을 명시해야 하고, AI 모델 다운로드는 인정되는 용도가 아니다.
해결책: foreground: false로 설정하고 AndroidManifest에서 FOREGROUND_SERVICE 완전 제거.
await FlutterGemma.installModel(
modelType: ModelType.gemmaIt,
fileType: model.fileType,
).fromNetwork(
url,
foreground: false, // FOREGROUND_SERVICE 미사용
).install();
Wi-Fi 환경에서는 백그라운드로도 충분히 다운로드된다.
삽질 3: HuggingFace 401 인증 오류
처음엔 Google 공식 레포(google/gemma-4-*)에서 모델을 받으려 했다.
401 Unauthorized
Google의 공식 Gemma 레포는 gated repo다. HuggingFace 계정으로 접근 신청을 하고 승인을 받아야 다운로드할 수 있다. 앱 사용자들이 각자 HuggingFace 계정을 만들고 신청하게 할 수는 없었다.
해결책: litert-community 공개 레포를 사용했다. 인증 없이 누구나 다운로드 가능하다.
https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm
https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it.litertlm
삽질 4: 두 번째 모델 다운로드가 0%에서 멈춘다
E2B 다운로드 → 채팅 테스트 → E4B 다운로드 시도.
E4B가 0%에서 멈췄다. E2B 때와 같은 증상이었지만 원인은 달랐다.
flutter_gemma는 getActiveModel()을 통해 모델을 메모리에 올린다. E2B 채팅을 끝내고 뒤로 가기를 눌러도 모델이 메모리에서 내려오지 않는다. 그 상태에서 E4B 다운로드를 시작하면 네이티브 레벨에서 리소스 충돌이 발생해 다운로드가 진행되지 않는다.
해결책: 다운로드 시작 전에 로드된 모델을 반드시 언로드한다.
Future<void> downloadModel(LocalAIModel model) async {
// 다운로드 전 로드된 모델 언로드
if (_inferenceModel != null) {
await unloadModel();
}
// ... 다운로드 시작
}
삽질 5: E2B 대화가 갑자기 안 된다
E2B 다운로드 → E2B 대화 성공 → E4B 다운로드 → E2B 대화 다시 시도 → 실패.
원인은 flutter_gemma의 active model 개념이었다.
flutter_gemma는 마지막으로 설치된 모델을 "active model"로 관리한다. getActiveModel()은 이 active model을 로드한다. E4B를 설치하면 E4B가 active model이 되고, E2B 채팅에 들어가도 E4B를 로드하려 한다.
해결책: 채팅 진입 전, 해당 모델을 fromFile()로 재설치해서 active model로 교체한다. 파일이 이미 있으므로 재다운로드는 없다.
Future<void> _activateModel(LocalAIModel model) async {
final dir = await getApplicationDocumentsDirectory();
final modelPath = p.join(dir.path, model.fileName);
// 이미 다운로드된 파일로 active model 교체
await FlutterGemma.installModel(
modelType: ModelType.gemmaIt,
fileType: model.fileType,
).fromFile(modelPath).install();
}
최종 구조
LocalAIService (싱글톤)
├── downloadModel() — 다운로드 전 unload + 진행률 스트림 + 타임아웃
├── loadModel() — _activateModel() 후 getActiveModel()
├── generateStream() — 스트리밍 추론 + 취소 지원
└── unloadModel() — InferenceModel.close() + 경쟁 조건 방어
모델은 enum으로 관리해서 새 모델 추가 시 항목 하나만 추가하면 된다.
핵심 체크리스트
| 항목 | 내용 |
|---|---|
FlutterGemma.initialize() | main()에서 runApp 전에 반드시 호출 |
foreground: false | FOREGROUND_SERVICE 권한 선언 불필요 |
| litert-community 레포 | 인증 없이 공개 모델 다운로드 가능 |
| 다운로드 전 unload | 로드된 모델이 있으면 먼저 해제 |
| fromFile()로 재활성화 | 다른 모델 설치 후 active model 교체 |
| 타임아웃 | 3분 내 진행 없으면 자동 실패 처리 |
마치며
온디바이스 AI는 분명히 가능성 있는 기술이다. 오프라인 동작, 개인정보 보호, 비용 절감 등 장점이 많다. 다만 모델 파일 크기(2~4GB)와 기기 RAM 요구사항(8GB+)이 현실적인 제약이다.
Gemma 4는 E2B 기준으로 모바일에서 충분히 사용할 수 있는 수준의 응답을 보여줬다. 앞으로 더 작고 성능 좋은 모델이 나오면 온디바이스 AI의 활용 범위는 더 넓어질 것 같다.
이 구현은 ttapp(모바일 Claude Code 원격 제어 앱)에 적용되어 있다. 관심 있는 분들은 ttapp에서 확인해볼 수 있다.
이 글 공유하기
// SPONSORED
[>]댓글
아직 댓글이 없어요. 첫 댓글을 남겨보세요!