본문 바로가기

LLM

스마트폰에서 AI를 돌리는 법 — 온디바이스 LLM 개발 입문 가이드

반응형

앱에 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년 모바일 개발의 다음 격전지예요. 😄


 

반응형