글 목록

온디바이스 모바일 AI 2026: Apple Foundation Models와 Gemini Nano 실전 가이드

iOS 26 Foundation Models 프레임워크와 Android Gemini Nano(ML Kit GenAI)로 클라우드 없이 단말에서 LLM을 실행하는 2026년 온디바이스 모바일 AI 구현을 Swift·Kotlin 코드와 함께 정리합니다.

IntentCode··15 min read

왜 지금 LLM을 폰 안으로 들이는가

생성형 AI 기능을 모바일 앱에 붙여 본 팀이라면 같은 청구서를 받아 봤을 겁니다. 사용자가 늘수록 추론 API 비용이 선형으로 늘고, 네트워크가 느린 곳에선 첫 토큰까지 1~2초씩 걸리며, 채팅 내용이 서버를 거치는 순간 개인정보 검토가 시작됩니다. 2026년의 흐름은 분명합니다. 요약·교정·분류·간단한 에이전트처럼 "작지만 자주 일어나는" 작업은 클라우드가 아니라 단말에서 처리하는 쪽으로 무게가 옮겨가고 있습니다.

배경에는 하드웨어의 도약이 있습니다. 최신 모바일 NPU는 70 TOPS를 넘는 연산 성능과 8~24GB의 통합 메모리를 갖췄고, 플래그십 단말은 30억 파라미터급 모델을 대화 가능한 속도로 돌립니다. 여기에 맞춰 애플과 구글이 각각 OS에 내장된 온디바이스 LLM을 서드파티 개발자에게 개방하면서, 이제 모델을 직접 번들하거나 추론 서버를 운영하지 않고도 몇 줄의 코드로 단말 내 생성형 AI를 호출할 수 있게 됐습니다.

이 글에서는 iOS 26의 Foundation Models 프레임워크(Swift)와 Android의 Gemini Nano + ML Kit GenAI API(Kotlin)를 중심으로, 온디바이스 AI를 실제 앱에 넣을 때 필요한 가용성 분기, 구조화 출력, 도구 호출, 그리고 클라우드 폴백까지를 코드와 함께 정리합니다.

배경: 두 플랫폼의 온디바이스 LLM 현황

애플과 구글의 접근은 결이 다릅니다. 애플은 "범용 텍스트 모델 + 타입 안전한 Swift API"를 제공하고, 구글은 "용도별 고수준 API(요약·교정·재작성) + 커스텀 프롬프트 API"라는 2층 구조로 갑니다.

항목Apple Foundation ModelsAndroid Gemini Nano (ML Kit GenAI)
도입 시점WWDC 2025 발표, iOS 26 / macOS 262025년 ML Kit GenAI API로 일반 개발자 개방
모델약 30억 파라미터 온디바이스 LLM (Apple Intelligence 동일 모델)Gemini Nano + 기능별 LoRA 어댑터
호출 방식FoundationModels 프레임워크, Swift APIAICore 시스템 서비스 위 ML Kit GenAI API
서드파티 입력텍스트 전용텍스트(일부 API 이미지 설명 지원)
추론 비용호출당 0원, 네트워크 불필요호출당 0원, 네트워크 불필요
강점구조화 출력(@Generable)·도구 호출 기본 제공요약/교정/재작성 등 검증된 고수준 API

핵심은 "어느 쪽이 더 똑똑하냐"가 아닙니다. 두 플랫폼 모두 클라우드 프런티어 모델(GPT/Gemini/Claude급)을 대체하려는 게 아니라, 자주 일어나는 가벼운 작업을 단말에서 끝내고 어려운 작업만 클라우드로 넘기는 하이브리드 전략을 전제로 설계됐습니다.

언제 단말에서 처리하고 언제 클라우드로 보낼지에 대한 실무 판단 기준은 다음과 같습니다.

기준온디바이스가 유리클라우드가 유리
데이터 민감도개인 메시지·건강·금융 등 외부 전송 부담비민감·공개 데이터
작업 난이도요약·분류·교정·짧은 추출복잡한 추론·대형 컨텍스트·최신 지식
지연 요구키보드·실시간 UI(수십 ms)수 초 허용
오프라인비행기·지하·해외 로밍항상 온라인 전제
비용 구조호출량이 많고 단가 압박호출량이 적고 품질 우선

심층 분석: Apple Foundation Models 다루기

1) 가용성 확인 — 모든 코드의 전제

온디바이스 모델은 "항상 있다"고 가정하면 안 됩니다. 단말이 Apple Intelligence 지원 기종이 아니거나, 사용자가 기능을 꺼 두었거나, 모델이 아직 내려받는 중일 수 있습니다. 그래서 세션을 만들기 전에 반드시 가용성을 먼저 확인합니다.

import FoundationModels

func makeSessionIfAvailable() -> LanguageModelSession? {
    let model = SystemLanguageModel.default

    switch model.availability {
    case .available:
        return LanguageModelSession()
    case .unavailable(let reason):
        switch reason {
        case .deviceNotEligible:
            // A17 Pro / M 시리즈 미만 등 — 클라우드 폴백으로 분기
            log("기기 미지원")
        case .appleIntelligenceNotEnabled:
            // 설정에서 Apple Intelligence 활성화 안내
            log("Apple Intelligence 비활성화")
        case .modelNotReady:
            // 모델 다운로드/준비 중 — 잠시 후 재시도
            log("모델 준비 중")
        @unknown default:
            log("알 수 없는 사유")
        }
        return nil
    }
}

이 분기를 건너뛰면 미지원 단말에서 런타임 오류로 이어집니다. 온디바이스 AI 코드는 "가용성 확인 → 분기 → 폴백"이 1번 패턴이라고 외워 두는 편이 좋습니다.

2) 기본 추론과 시스템 지시문

세션은 대화의 맥락(context)을 들고 있는 객체입니다. 한 번 만든 세션에 이어서 질의하면 멀티턴이 유지됩니다.

// 역할/톤을 지시문으로 고정한 세션
let session = LanguageModelSession(
    instructions: """
    당신은 한국어 쇼핑 앱의 도우미입니다.
    답변은 항상 존댓말로, 3문장 이내로 간결하게 작성합니다.
    """
)

let reply = try await session.respond(
    to: "이 후기들을 한 문장으로 요약해줘: \(reviews)"
)
print(reply.content)   // 생성된 텍스트

지시문(instructions)은 시스템 프롬프트에 해당하며, 사용자 입력(prompt)과 분리해 두는 것이 안전합니다. 사용자 입력을 지시문에 직접 끼워 넣으면 프롬프트 인젝션 위험이 커집니다.

3) 구조화 출력 — @Generable로 JSON 파싱 없애기

온디바이스 모델의 출력을 앱 로직에 바로 쓰려면 자유 텍스트가 아니라 타입이 보장된 구조체가 필요합니다. Foundation Models는 @Generable/@Guide 매크로와 제약 디코딩(constrained decoding)으로 출력의 구조적 정확성을 보장합니다. "JSON으로 답해줘"라고 빌고 정규식으로 파싱하던 시대를 끝내는 기능입니다.

@Generable
struct ReviewInsight {
    @Guide(description: "후기의 핵심 감정")
    let sentiment: Sentiment

    @Guide(description: "1점부터 5점까지의 만족도", .range(1...5))
    let score: Int

    @Guide(description: "개선이 필요한 키워드 최대 3개")
    let issues: [String]
}

@Generable
enum Sentiment: String {
    case positive, neutral, negative
}

let result = try await session.respond(
    to: "다음 후기를 분석해줘: \(text)",
    generating: ReviewInsight.self
)

let insight = result.content   // 이미 ReviewInsight 타입
updateUI(score: insight.score, issues: insight.issues)

@Guide.range(1...5) 같은 제약은 모델이 범위를 벗어난 값을 만들지 못하도록 디코딩 단계에서 강제합니다. 후처리 검증 코드를 크게 줄여 줍니다.

4) 스트리밍과 도구 호출(Tool Calling)

긴 답변은 스트리밍으로 부분 출력을 받아 UI에 즉시 흘려보내고, 모델이 모르는 정보(현재 위치·실시간 데이터)는 도구 호출로 앱 함수에 위임합니다.

// 스트리밍 — 부분 응답을 순차적으로 수신
let stream = session.streamResponse(to: "여행 일정을 만들어줘")
for try await partial in stream {
    updateTextView(partial.content)   // 누적 스냅샷을 그대로 렌더
}
// 도구 호출 — 모델이 필요할 때 앱 함수를 부른다
struct WeatherTool: Tool {
    let name = "getWeather"
    let description = "도시 이름으로 현재 기온을 조회합니다"

    @Generable
    struct Arguments {
        @Guide(description: "도시 이름 (예: 서울)")
        let city: String
    }

    func call(arguments: Arguments) async throws -> ToolOutput {
        let temp = try await WeatherAPI.currentTemp(in: arguments.city)
        return ToolOutput("\(arguments.city)의 현재 기온은 \(temp)℃입니다.")
    }
}

let session = LanguageModelSession(tools: [WeatherTool()])
let reply = try await session.respond(to: "서울 날씨에 맞는 옷을 추천해줘")

도구 호출 덕분에 온디바이스 모델도 "현재 상태를 반영한" 답을 만들 수 있습니다. 모델은 추론만 하고, 실제 데이터 접근은 앱이 책임지는 구조입니다.

심층 분석: Android Gemini Nano 다루기

Android는 AICore 시스템 서비스 위에서 동작합니다. AICore는 Private Compute Core 원칙을 따라 인터넷 직접 접근이 없고, 요청을 격리하며, 처리 후 입력·출력을 저장하지 않습니다. 그 위에 ML Kit GenAI가 요약·교정·재작성·이미지 설명 같은 고수준 API를 제공하고, 각 API는 품질을 위해 Gemini Nano 베이스 모델에 기능별 LoRA 어댑터를 얹어 동작합니다.

1) 기능 가용성 확인과 모델 다운로드

iOS와 마찬가지로 Android에서도 기능이 즉시 사용 가능하지 않을 수 있습니다. AICore가 모델·어댑터를 내려받아야 하는 경우가 있어, 상태 확인 → 필요 시 다운로드 → 추론 순서를 지킵니다.

val options = SummarizerOptions.builder(context)
    .setInputType(InputType.ARTICLE)
    .setOutputType(OutputType.THREE_BULLETS)
    .setLanguage(Language.ENGLISH)
    .build()

val summarizer = Summarization.getClient(options)

when (summarizer.checkFeatureStatus().await()) {
    FeatureStatus.AVAILABLE -> runSummary(summarizer, article)
    FeatureStatus.DOWNLOADABLE,
    FeatureStatus.DOWNLOADING -> {
        // 진행률 콜백으로 UX 안내 후, 완료되면 추론
        summarizer.downloadFeature(object : DownloadCallback {
            override fun onDownloadCompleted() = runSummary(summarizer, article)
            override fun onDownloadFailed(e: GenAiException) = fallbackToCloud()
            override fun onDownloadProgress(bytes: Long) { /* UI 갱신 */ }
            override fun onDownloadStarted(total: Long) { /* UI 갱신 */ }
        })
    }
    FeatureStatus.UNAVAILABLE -> fallbackToCloud()  // 미지원 단말
}

setLanguage로 출력 언어를 지정하지만, GenAI API의 지원 언어는 기능·버전에 따라 다릅니다. 영어를 중심으로 출발해 점차 확대되는 단계이므로, 한국어가 필요한 경우 지원 여부를 확인하고 미지원 시 클라우드로 폴백하는 분기를 함께 둡니다.

2) 추론 실행

상태가 AVAILABLE이면 요약을 실행합니다. 요약 결과는 출력 타입에 맞춰(예: 불릿 3개) 반환됩니다.

fun runSummary(summarizer: Summarizer, article: String) {
    val request = SummarizationRequest.builder(article).build()

    summarizer.runInference(request) { result ->
        // OutputType.THREE_BULLETS → 불릿 형태 요약
        showBullets(result.summary)
    }
}

요약·교정·재작성처럼 정형화된 작업은 이 고수준 API만으로 충분합니다. 자유로운 프롬프트가 필요하면 ML Kit의 실험적 Prompt API로 Gemini Nano에 직접 프롬프트를 보낼 수 있지만, 출력 형식·길이가 고수준 API보다 변동성이 크므로 검증 로직을 더 두는 편이 안전합니다.

실전 가이드: 하이브리드 라우팅 설계

온디바이스 단독으로 모든 걸 해결하려 하지 마세요. 현실적인 구조는 단말에서 먼저 시도하고, 가용성·난이도·신뢰도 기준에 미달하면 클라우드로 승격하는 계층형(tiered) 라우팅입니다.

사용자 입력
   │
   ├─ 가용성 OK & 가벼운 작업? ──예──▶ 온디바이스 추론
   │                                      │
   │                              신뢰도/형식 검증 통과? ──예──▶ 반환
   │                                      │아니오
   └─ 아니오 ───────────────────────────▶ 클라우드 LLM ──▶ 반환

라우팅 정책은 코드에 흩뿌리지 말고 한 곳에서 설정으로 관리합니다.

# config/inference-routing.yaml
on_device:
  enabled: true
  tasks: [summarize, proofread, classify, extract]   # 단말에서 끝낼 작업
  max_input_chars: 4000                               # 초과 시 클라우드 승격
  timeout_ms: 1500                                    # 지연 상한
fallback:
  provider: cloud
  model: claude-haiku-4-5                             # 가벼운 폴백
  escalate_model: claude-opus-4-7                     # 고난도 승격
privacy:
  never_send_to_cloud: [health, payment, message_body]  # 단말 외 전송 금지 분류

핵심 원칙 세 가지입니다. ① 가용성 분기를 1번 단계로 두고, ② 단말 처리 결과가 형식·신뢰도 기준에 미달하면 클라우드로 승격(escalate) 하며, ③ 민감 분류 데이터는 정책상 단말 밖으로 내보내지 않습니다. 이 구조는 비용을 누르면서도 품질 하한을 클라우드로 보장합니다.

빌드 측면에서는 단말 요구사항을 명확히 합니다. iOS는 Apple Intelligence 지원 기종(A17 Pro·M 시리즈 계열)과 iOS 26 이상이 전제이고, Android는 AICore가 동작하는 단말(예: Pixel 9 시리즈, Galaxy S25, Xiaomi 15 등 MediaTek Dimensity·Qualcomm Snapdragon·Google Tensor 최적화 단말)이 전제입니다. 미지원 단말 비중이 크다면 클라우드 폴백이 사실상 주 경로가 될 수 있으니, 초기부터 폴백을 1급 시민으로 설계하세요.

트러블슈팅

1) "분명 코드는 맞는데 미지원 단말에서 죽는다" 가용성 확인 없이 LanguageModelSession()을 바로 생성했을 가능성이 큽니다. iOS는 SystemLanguageModel.default.availability, Android는 checkFeatureStatus()세션/추론 전에 반드시 호출하고, unavailable·UNAVAILABLE 경로에서 클라우드 폴백으로 빠지도록 하세요. 온디바이스는 "있으면 좋은 것"이지 "항상 있는 것"이 아닙니다.

2) 백그라운드 전환 시 추론이 끊기거나 앱이 종료된다 온디바이스 추론은 메모리·연산을 크게 쓰므로, 사용자가 앱을 백그라운드로 보내면 OS가 작업을 중단하거나 메모리 회수를 할 수 있습니다. 긴 추론은 가급적 포그라운드에서 수행하고, 입력 길이를 제한(max_input_chars)하며, 백그라운드 전환 시 진행 중 작업을 취소하고 재개 가능한 상태로 저장하는 방어 코드를 둡니다.

3) 출력 형식이 들쭉날쭉해 파싱이 깨진다 자유 텍스트를 정규식으로 파싱하고 있다면 구조화 출력으로 바꾸세요. iOS는 @Generable/@Guide로 타입과 범위를 강제하고, Android는 요약·교정 같은 고수준 API의 정형 출력을 우선 사용합니다. 자유 프롬프트(Prompt API)를 쓸 수밖에 없다면 출력 스키마 검증과 1회 재시도 로직을 함께 둡니다.

결론: IntentCode의 관점

2026년 모바일 AI의 경쟁력은 "가장 큰 모델을 부르는 것"이 아니라 "어떤 작업을 어디서 처리할지 정확히 라우팅하는 것" 에 있습니다. 자주 일어나는 가벼운 작업은 단말에서 0원·수십 ms·오프라인으로 끝내고, 어려운 작업만 클라우드로 승격하면 비용·지연·프라이버시를 동시에 잡을 수 있습니다. IntentCode는 온디바이스 모바일 AI(iOS Foundation Models·Android Gemini Nano)와 클라우드 네이티브 추론을 하나의 하이브리드 라우팅으로 묶어, 민감 데이터는 단말에 남기고 품질 하한은 클라우드로 보장하는 구조를 고객 앱에 이식합니다. 단말과 클라우드는 양자택일이 아니라, 같은 파이프라인의 두 계층입니다.