Flutter에서 로컬 LLM 연동하기 — 서버 없이 온디바이스로 AI 돌리기
앱에 AI 기능을 넣을 때 보통은 Claude 같은 서버 API를 부른다. 그런데 호출량이 늘수록 비용이 따라 늘고, 오프라인에서는 아예 동작하지 않는다는 한계가 있다. 그래서 이번에는 반대 방향을 실험해 봤다. 서버 없이, 스마트폰 안에서 직접 도는 소형 LLM을 Flutter 앱에 붙이는 것이다. 결론부터 말하면 "생각보다 잘 되지만, 쓸 곳을 잘 골라야 한다"였다. 실제로 해본 순서대로 정리한다.
1. 온디바이스 추론이 되는 조건
모든 기기에서 모든 모델이 도는 것은 아니다. 관건은 모델 파일 크기와 기기 RAM이다. 대략의 감각은 이렇다.
- 모델은 4bit 양자화 기준으로 파라미터 수의 절반 남짓한 용량이 된다. 1B 모델이면 수백 MB, 4B급이면 2~4GB 수준의 파일을 앱이 내려받아 들고 있어야 한다.
- 추론 중에는 모델 크기 이상의 메모리가 필요하다. RAM 8GB 이상의 비교적 최신 기기라면 2~4B급, 보급형 기기라면 1B 이하가 현실적인 선이다.
- 모델 파일을 APK에 넣으면 스토어 용량 제한에 걸리므로, 첫 실행 시 다운로드하는 구조가 일반적이다.
2. 쓸 수 있는 소형 모델 라인업
온디바이스용으로 변환·공개된 모델은 이미 선택지가 꽤 넓다. 대표적인 것들을 규모순으로 놓으면 이렇다.
- Gemma 3n E2B/E4B: 구글이 온디바이스를 겨냥해 만든 모델. 실효 파라미터 2B/4B로 동작하도록 선택적 활성화 기법을 써서, 체급 대비 메모리 부담이 작다. 텍스트 외에 이미지·오디오 입력까지 받는 멀티모달이라는 점이 특징이다.
- Gemma 3 1B: 텍스트 전용 경량 모델. 보급형 기기까지 커버하려면 이쪽이 무난하다.
- Qwen3 0.6B, SmolLM 등: 더 가벼운 대안. 품질 기대치는 낮춰야 하지만 파이프라인 검증용으로 좋다.
- 이 밖에 Phi 계열, DeepSeek R1 distill 등도 온디바이스용 변환본이 공개되어 있다.
모델 파일은 MediaPipe용 .task 형식이나 LiteRT 계열 형식으로 배포된다. Hugging Face에서 받을 수 있는데, Gemma 계열은 라이선스 동의와 액세스 토큰이 필요한 경우가 있다.
3. Flutter 연동 — flutter_gemma로 시작
연동 방식은 크게 세 가지가 있다. ① 커뮤니티 플러그인 사용, ② MediaPipe LLM Inference API를 플랫폼 채널로 직접 호출, ③ llama.cpp 같은 네이티브 라이브러리를 FFI로 바인딩. 처음이라면 답은 사실상 정해져 있다. 플러그인이다. 나는 flutter_gemma 패키지를 썼다.
구조를 보면 Flutter 앱 아래에 플러그인이 있고, 그 아래에서 MediaPipe/LiteRT 런타임이 모델 파일을 실행한다. 네이티브 코드를 직접 만질 일은 없다. 사용 흐름은 대략 이렇다 (패키지 버전에 따라 API 이름은 달라질 수 있으니 공식 문서를 기준으로 삼자).
dependencies:
flutter_gemma: ^0.16.0 # 버전은 pub.dev에서 최신 확인
// 1) 모델 파일 설치 (첫 실행 시 다운로드해 둔 경로 지정)
final gemma = FlutterGemmaPlugin.instance;
await gemma.modelManager.setModelPath(modelFilePath);
// 2) 모델 로드
final model = await gemma.createModel(
modelType: ModelType.gemmaIt,
maxTokens: 512,
);
// 3) 채팅 세션 만들고 추론
final chat = await model.createChat();
await chat.addQueryChunk(Message.text(text: '한 문장으로 자기소개해 줘', isUser: true));
// 4) 스트리밍으로 토큰 받기
chat.generateChatResponseAsync().listen((token) {
// UI에 이어 붙이기 — 서버 SSE 처리와 같은 요령
});
흥미로운 점은, 마지막 단계가 서버 API의 SSE 스트리밍 처리와 거의 같은 모양이라는 것이다. 토큰이 올 때마다 마지막 메시지에 이어 붙이고 화면을 갱신한다. 채팅 UI 코드는 서버 버전을 그대로 재사용할 수 있었다.
4. 성능·배터리의 현실
장밋빛으로만 쓸 수는 없다. 실제로 돌려 보면 다음 현실을 만난다.
- 첫 로드가 느리다: 모델을 메모리에 올리는 데 기기에 따라 수 초에서 수십 초가 걸린다. 앱 시작 시 백그라운드에서 미리 로드하고, 로딩 UI를 반드시 두자.
- 생성 속도는 기기 편차가 크다: 최신 플래그십과 몇 년 된 보급형의 토큰 생성 속도는 몇 배씩 차이 난다. GPU 백엔드를 켤 수 있는 기기에서는 CPU보다 확연히 빨라진다.
- 발열과 배터리: 연속으로 긴 답변을 생성시키면 기기가 눈에 띄게 따뜻해진다. 짧은 작업 위주로 설계하고, 긴 생성 작업을 연달아 시키는 UX는 피하는 것이 좋다.
- 품질 기대치: 2~4B급 모델은 요약, 분류, 짧은 초안 작성, 간단한 Q&A까지는 쓸 만하지만, 복잡한 추론이나 정확성이 중요한 작업에서는 대형 모델과 체감 차이가 분명하다.
5. 서버 방식과의 선택 기준
그래서 온디바이스와 서버 API 중 무엇을 쓸 것인가. 표로 놓고 보면 판단이 쉬워진다.
내 기준은 이렇게 정리됐다.
- 온디바이스가 맞는 경우: 오프라인 동작이 필요할 때, 개인정보가 기기 밖으로 나가면 안 될 때, 호출량이 많아 API 비용이 부담될 때, 그리고 작업 자체가 요약·분류처럼 가벼울 때.
- 서버 API가 맞는 경우: 답변 품질이 서비스의 핵심일 때, 구형 기기까지 지원해야 할 때, 앱 용량과 모델 다운로드 UX를 감당하기 어려울 때.
- 혼합 구조: 실전에서 가장 설득력 있는 답. 가벼운 작업은 온디바이스로 처리해 비용을 아끼고, 품질이 필요한 요청만 서버로 보낸다. 오프라인이면 온디바이스로 폴백하는 구조도 가능하다.
서버 없이 내 앱 안에서 LLM이 도는 경험은 처음 해 보면 꽤 신선하다. 모델 파일 하나와 플러그인 하나로 시작할 수 있으니, 가벼운 기능 하나부터 실험해 보길 권한다.
함께 보면 좋은 글: Flutter 앱에 Claude API 연동하기 — 채팅 UI부터 스트리밍 응답까지
※ 이 글의 모델 라인업, 패키지 버전, 지원 형식은 2026년 기준이며 변경될 수 있습니다.
댓글
댓글 쓰기