앱에 AI 기능을 붙이려면 항상 클라우드 API를 써야 할까요?
아니에요. 2026년 지금은 스마트폰 자체에서 LLM을 돌릴 수 있어요. 인터넷 없이, API 비용 없이, 사용자 데이터가 서버로 나가지 않게요.
이번 글에서는 온디바이스 AI가 뭔지, 왜 필요한지, 그리고 Android·iOS·Flutter로 실제로 어떻게 구현하는지 코드와 함께 정리해 드릴게요.
왜 온디바이스 AI인가
클라우드 AI의 문제점이 네 가지예요.
레이턴시 — 클라우드 API를 호출하면 네트워크 왕복 시간이 수백 밀리초 추가돼요. 실시간 느낌이 깨져요.
프라이버시 — 사용자 데이터가 서버로 전송돼요. 의료, 금융, 개인 메모 같은 민감한 데이터를 다루는 앱은 이게 치명적이에요.
비용 — API 호출마다 돈이 나가요. 사용자가 많아질수록 서버 비용이 폭증해요.
오프라인 — 인터넷이 없으면 동작 안 해요.
온디바이스 AI는 이 네 가지를 한 번에 해결해요.
온디바이스 AI의 장점:
- 레이턴시: <5ms (vs 클라우드 200~500ms)
- 프라이버시: 데이터가 기기 밖으로 나가지 않음
- 비용: API 호출 비용 없음
- 오프라인: 인터넷 없이 동작
단점도 있어요. 모델 크기와 성능에 제한이 있고, 배터리를 더 써요. 복잡한 추론이 필요한 작업은 여전히 클라우드가 유리해요.
스마트폰의 하드웨어 한계
온디바이스 개발의 핵심은 제약 이해예요.
메모리 대역폭 — 데이터센터 GPU(H100)는 초당 3.3TB를 처리해요. 스마트폰은 50~90GB예요. 30~50배 차이예요. LLM 추론은 메모리 대역폭에 병목이 걸려서 이 차이가 그대로 속도 차이가 돼요.
가용 RAM — 스마트폰 스펙이 8~16GB여도 OS 오버헤드를 제하면 실제 사용 가능한 메모리는 4GB 이하인 경우가 많아요.
발열과 배터리 — 지속적인 LLM 추론은 배터리를 빠르게 소모하고 발열이 생겨요. 서멀 스로틀링이 걸리면 속도가 급격히 떨어져요.
이 제약 때문에 온디바이스 LLM은 양자화(Quantization) 가 필수예요.
양자화 효과:
FP32 (32비트) → INT4 (4비트): 메모리 8배 감소
메모리 감소 = 메모리 트래픽 감소 = 토큰 생성 속도 향상
3B 모델 기준:
- FP32: 12GB RAM 필요
- INT4: 1.5GB RAM 필요 → 스마트폰에서 실행 가능
온디바이스에 적합한 모델들
2026년 기준으로 스마트폰에서 실용적으로 쓸 수 있는 모델들이에요.
모델 파라미터 RAM 필요량 특징
| Gemma 4 E2B | 2.3B | ~2GB | 음성 입력 지원, 구글 공식 |
| Gemma 4 E4B | 4.5B | ~4GB | 멀티모달, 128K 컨텍스트 |
| Llama 3.2 1B | 1B | ~700MB | 초경량, 빠른 속도 |
| Llama 3.2 3B | 3B | ~2GB | 성능/크기 균형 |
| Qwen3 0.6B | 0.6B | ~400MB | 극경량, 기본 태스크 |
| Phi-4 Mini | 3.8B | ~2.5GB | Microsoft, 추론 강함 |
흥미로운 연구 결과가 있어요. Llama 3.2 1B에 트리 서치 전략을 적용하면 8B 모델을 이기는 경우가 있어요. 모델 크기보다 추론 시 더 많은 컴퓨팅을 쓰는 전략이 효과적일 수 있다는 거예요.
플랫폼별 개발 방법
Android — ExecuTorch + MediaPipe LLM API
MediaPipe LLM Inference API — 구글이 공식 지원하는 가장 쉬운 방법이에요.
// build.gradle
dependencies {
implementation 'com.google.mediapipe:tasks-genai:0.10.14'
}
import com.google.mediapipe.tasks.genai.llminference.LlmInference
class OnDeviceLLM(context: Context) {
private lateinit var llmInference: LlmInference
fun initialize() {
val options = LlmInference.LlmInferenceOptions.builder()
.setModelPath("/data/local/tmp/gemma-4-e4b-int4.bin")
.setMaxTokens(1024)
.setTopK(40)
.setTemperature(0.8f)
.setRandomSeed(101)
.build()
llmInference = LlmInference.createFromOptions(context, options)
}
fun generateResponse(prompt: String): String {
return llmInference.generateResponse(prompt)
}
// 스트리밍 방식 (더 자연스러운 UX)
fun generateResponseAsync(
prompt: String,
onPartialResult: (String, Boolean) -> Unit
) {
llmInference.generateResponseAsync(prompt, onPartialResult)
}
}
// Activity에서 사용
class MainActivity : AppCompatActivity() {
private val llm = OnDeviceLLM(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
llm.initialize()
// 스트리밍으로 토큰 실시간 표시
llm.generateResponseAsync("파이썬 리스트 정렬 방법 알려줘") { partial, done ->
runOnUiThread {
textView.append(partial)
if (done) textView.append("\n[완료]")
}
}
}
}
}
ExecuTorch — 더 세밀한 제어가 필요할 때 써요. Meta가 Instagram, WhatsApp에서 실제로 쓰는 프레임워크예요.
# 모델을 ExecuTorch 형식으로 변환 (Python)
from executorch.exir import to_edge
from torch.export import export
model = load_llama_model("Llama-3.2-1B")
exported = export(model, example_inputs)
edge_program = to_edge(exported)
executorch_program = edge_program.to_executorch()
with open("llama.pte", "wb") as f:
f.write(executorch_program.buffer)
iOS — Apple Foundation Models + Core ML
Apple Foundation Models — iOS 26+에서 사용 가능. 시스템 모델을 직접 호출하는 가장 쉬운 방법이에요.
import FoundationModels
struct ContentView: View {
@State private var response = ""
var body: some View {
VStack {
Text(response)
Button("AI에게 물어보기") {
Task { await askAI() }
}
}
}
func askAI() async {
let session = LanguageModelSession()
let prompt = "파이썬 딕셔너리 사용법 간단히 설명해줘"
do {
for try await token in session.streamResponse(to: prompt) {
await MainActor.run {
response += token
}
}
} catch {
print("오류: \(error)")
}
}
}
Core ML + llama.cpp — 커스텀 모델을 쓸 때예요.
class LlamaCppWrapper {
private var context: OpaquePointer?
func loadModel(path: String) {
var params = llama_model_default_params()
params.n_gpu_layers = 99 // Apple Silicon GPU 최대 활용
let model = llama_load_model_from_file(path, params)
var ctxParams = llama_context_default_params()
ctxParams.n_ctx = 2048
context = llama_new_context_with_model(model, ctxParams)
}
func generate(prompt: String, maxTokens: Int = 256) -> String {
var result = ""
// llama.cpp API 호출
return result
}
}
Flutter — flutter_gemma + llama.cpp FFI
Flutter로 iOS/Android 동시 개발할 때예요. 코드 한 번으로 양쪽 다 돌아가는 게 가장 큰 장점이에요.
방법 1: flutter_gemma (가장 쉬움)
구글이 공식 지원하는 Flutter 패키지예요. Gemma 계열 모델에 특화돼 있어요.
# pubspec.yaml
dependencies:
flutter_gemma: ^0.2.0
import 'package:flutter_gemma/flutter_gemma.dart';
class OnDeviceChatPage extends StatefulWidget {
@override
State<OnDeviceChatPage> createState() => _OnDeviceChatPageState();
}
class _OnDeviceChatPageState extends State<OnDeviceChatPage> {
final FlutterGemmaPlugin _gemma = FlutterGemmaPlugin.instance;
final TextEditingController _controller = TextEditingController();
String _response = '';
bool _isLoading = false;
@override
void initState() {
super.initState();
_initModel();
}
Future<void> _initModel() async {
await _gemma.init(
modelPath: 'assets/gemma-4-e4b-it-int4.bin',
maxTokens: 512,
);
}
Future<void> _sendMessage(String prompt) async {
setState(() {
_isLoading = true;
_response = '';
});
final stream = _gemma.generateResponseStream(prompt);
await for (final token in stream) {
setState(() {
_response += token;
});
}
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('온디바이스 AI')),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
children: [
Text(_response),
if (_isLoading) CircularProgressIndicator(),
],
),
),
),
Padding(
padding: EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: '질문 입력'),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: () {
_sendMessage(_controller.text);
_controller.clear();
},
),
],
),
),
],
),
);
}
}
방법 2: llama.cpp FFI (더 많은 모델 지원)
Gemma 외에 Llama, Qwen 등 다양한 GGUF 모델을 쓰고 싶을 때예요.
# pubspec.yaml
dependencies:
ffi: ^2.1.0
path_provider: ^2.1.0
// 실용적인 방법: MethodChannel로 네이티브 코드 호출
class LlamaFlutterService {
static const _channel = MethodChannel('llama_cpp_channel');
static const _eventChannel = EventChannel('llama_cpp_stream');
Future<void> loadModel(String modelPath) async {
await _channel.invokeMethod('loadModel', {'path': modelPath});
}
Future<String> generate(String prompt) async {
final result = await _channel.invokeMethod('generate', {
'prompt': prompt,
'maxTokens': 256,
});
return result as String;
}
// 스트리밍은 EventChannel 사용
Stream<String> generateStream(String prompt) {
return _eventChannel
.receiveBroadcastStream({'prompt': prompt})
.map((event) => event as String);
}
}
모델 파일 배포 방법
class ModelDownloader {
Future downloadModel() async {
final dir = await getApplicationDocumentsDirectory();
final modelPath = '${dir.path}/gemma-4-e4b-q4.bin';
if (File(modelPath).existsSync()) return modelPath;
// 허깅페이스에서 다운로드
final response = await http.get(
Uri.parse('https://huggingface.co/.../gemma-4-e4b-q4.bin'),
);
await File(modelPath).writeAsBytes(response.bodyBytes);
return modelPath;
}
}
Flutter 온디바이스 AI 방법 비교
방법 지원 모델 난이도 추천 용도
| flutter_gemma | Gemma 계열만 | 쉬움 | 빠른 시작 |
| llama.cpp FFI | GGUF 모든 모델 | 어려움 | 다양한 모델 |
| MethodChannel | 모든 모델 | 중간 | 프로덕션 |
Flutter는 처음엔 flutter_gemma로 시작하고, 다양한 모델이 필요하면 MethodChannel 방식으로 네이티브 코드를 따로 짜는 게 현실적이에요.
실전 최적화 팁
양자화 선택 기준
모델 크기별 권장 양자화:
1B 이하: INT8 (품질 손실 최소화)
1~3B: INT4 (속도와 품질 균형)
3B 이상: INT4 또는 INT3 (메모리 절약 우선)
GGUF 파일명 규칙:
- Q8_0: 8비트 양자화 (고품질)
- Q4_K_M: 4비트 양자화 (중간 품질, 가장 많이 씀)
- Q3_K_S: 3비트 양자화 (경량)
프리픽스 캐싱
시스템 프롬프트가 길다면 프리픽스 캐싱으로 반복 계산을 줄일 수 있어요.
// 시스템 프롬프트를 초기화 시 한 번만 처리
val systemPrompt = """
당신은 코딩 도우미입니다.
파이썬, 코틀린, 다트 전문가입니다.
항상 간결하게 답변하세요.
""".trimIndent()
llmInference.prefillPrompt(systemPrompt)
llmInference.generateResponse("리스트 정렬 방법") // 사용자 입력만 추가
배터리 최적화
class LLMManager(private val context: Context) {
private val wakeLock = (context.getSystemService(Context.POWER_SERVICE)
as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LLMManager::WakeLock"
)
fun startInference() {
wakeLock.acquire(10 * 60 * 1000L) // 최대 10분
}
fun stopInference() {
if (wakeLock.isHeld) wakeLock.release()
}
}
어떤 태스크에 적합한가
온디바이스가 잘 맞는 것
- 텍스트 요약 (메모, 이메일)
- 간단한 Q&A
- 코드 자동완성
- 감정 분석, 키워드 추출
- 오프라인 챗봇
- 번역 (소형 모델)
클라우드가 더 적합한 것
- 복잡한 멀티스텝 추론
- 최신 정보가 필요한 답변
- 긴 문서 처리 (수만 토큰)
- 이미지·영상 생성
시작하는 가장 빠른 방법
1단계: llama.cpp로 데스크탑에서 먼저 테스트
# macOS
brew install llama.cpp
# Gemma 4 E4B 모델 다운로드
huggingface-cli download google/gemma-4-e4b-it-GGUF \
gemma-4-e4b-it-Q4_K_M.gguf
# 실행
llama-cli -m gemma-4-e4b-it-Q4_K_M.gguf -p "안녕하세요" -n 128
2단계: Ollama로 로컬 API 서버 띄우기
ollama run gemma4:e4b
curl http://localhost:11434/api/generate -d '{
"model": "gemma4:e4b",
"prompt": "파이썬 리스트 정렬 방법"
}'
3단계: 모바일 앱에 통합
데스크탑에서 쓸만하다고 확인했으면 플랫폼에 맞게 올려요.
Android: MediaPipe LLM API 또는 ExecuTorch
iOS: Apple Foundation Models 또는 Core ML
Flutter: flutter_gemma 또는 MethodChannel
마무리
온디바이스 AI 개발의 핵심을 세 줄로 정리하면 이래요.
모델 선택 — 1~3B 파라미터, INT4 양자화가 2026년 스마트폰의 스위트스팟이에요.
프레임워크 선택 — Android 프로덕션은 ExecuTorch, iOS는 Foundation Models, Flutter는 flutter_gemma로 시작해요.
유스케이스 선택 — 요약, 분류, 간단한 Q&A는 온디바이스가 클라우드보다 낫고, 복잡한 추론은 하이브리드(온디바이스 + 클라우드)로 가요.
API 비용 없이, 프라이버시 걱정 없이, 오프라인에서도 동작하는 AI 앱 — 이게 2026년 모바일 개발의 다음 격전지예요. 😄
'LLM' 카테고리의 다른 글
| SGLang launch_server 파라미터 완전 정리 (0) | 2026.04.09 |
|---|---|
| SGLang 서빙에 대한 모든 것 — 설치부터 프로덕션까지 완전 가이드 (0) | 2026.04.09 |
| Grok 5 완전 정리 — 6조 파라미터, AGI 10%, 역대 최대 AI의 진실 (0) | 2026.04.08 |
| Anthropic이 숨기려 했던 AI — Claude Mythos 유출 사건 완전 정리 (0) | 2026.04.08 |
| 13조 원 투자한 파트너 대체하는 Microsoft의 AI 독립 선언 (0) | 2026.04.08 |