본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 12:13

샌드박스 속의 유령: React Native와 Gemma 4를 활용한 오프라인 AI 코치 설계하기

요약

React Native 환경에서 Gemma 4와 LiteRT-LM 스택을 활용하여 100% 오프라인으로 작동하는 모바일 AI 코치를 설계하는 과정을 다룹니다. 클라우드 API 비용과 네트워크 지연 문제를 해결하기 위해 온디바이스(On-device) AI 아키텍처를 구축하는 엔지니어링적 도전과 해결책을 제시합니다.

핵심 포인트

  • 클라우드 API 의존성을 제거한 주권적 엣지(Sovereign Edge) 아키텍처 설계
  • LiteRT-LM 스택을 활용한 모바일 온디바이스 AI 구현
  • 오프라인 환경에서의 개인정보 보호 및 API 비용 절감 효과
  • 모바일 메모리 제한 환경에서의 모델 최적화 필요성

새벽 3시 14분이었습니다. 방 안의 정적은 노트북 팬의 웅웅거리는 소리에 의해서만 깨질 뿐, 완벽하게 고요했습니다. 책상 위에는 두 대의 테스트 기기가 놓여 있었습니다. Android를 실행 중인 Xiaomi MI와 제가 매일 사용하는 iPhone 15였습니다. 두 화면 모두 완전히 검은색이었습니다.

그 옆에는 김이 빠진 물이 절반쯤 담긴 유리잔이 놓여 있었습니다. 이보다 더한 아이러니는 없었습니다.

지난 3주 동안 저는 개인 사이드 프로젝트의 엔지니어링 참호 속에서 고난도의 설계 작업을 수행하고 있었습니다. 바로 완전히 주권적(sovereign)이고 100% 오프라인으로 작동하는 대화형 AI 수분 보충 코치(Hydration Coach)를 애플리케이션의 핵심부에 직접 내장하는 것이었습니다. 클라우드 서버도, API 게이트웨이도, 네트워크 지연 시간(latency)도 없어야 했습니다. 그리고 제 자금을 고갈시키는 월간 토큰 구독료도 전혀 없어야 했습니다.

그 약속은 매우 매혹적이었습니다. 사용자가 셀룰러 서비스가 전혀 없는 고립된 산길에 있더라도, iOS 또는 Android에서 Water Tracker with Subra AI를 열고, 코치에게 열 유체 손실(thermal fluid loss)에 대해 질문하면 매우 맥락적이고 실시간적인 답변을 받을 수 있게 되는 것입니다.

하지만 새벽 3시 15분, 모니터의 로그 스트림은 저에게 맥락을 제공하지 않았습니다. 대신 단 하나의 파괴적인 에러 라인을 반복해서 내뱉고 있었습니다.

Fatal Exception: libc++abi: terminating with uncaught exception of type std::runtime_error: Failed to map model memory

앱은 단순히 충돌하는 것이 아니라 질식하고 있었습니다. 저는 LLM 유령을 저의 React Native 샌드박스로 초대했고, 그 유령은 메모리에 굶주려 있었습니다.

제1장: 주권적 에지 아키텍처 (The Sovereign Edge Architecture)

제가 어떻게 이 상황에 이르게 되었는지 이해하려면 설계 청사진을 살펴봐야 합니다. 생성형 AI (GenAI) 모바일 기능을 구축하는 대부분의 제품 팀은 편안한 길을 택합니다. 원격 클라우드 API 엔드포인트에 axios fetch 요청을 감싸는 방식입니다.

사용자가 엘리베이터, 지하철 터널, 또는 비행기에 탑승하기 전까지는 우아해 보입니다. 하지만 그 순간, 당신의 "스마트"한 코치는 값비싼 로딩 스피너(loading spinner)로 전락합니다. 더 나쁜 점은, 사용자가 앱과 가볍게 채팅을 나눌 때마다 당신에게 비용이 청구된다는 것입니다. 만약 당신의 앱이 바이럴(viral)을 탄다면, API 비용은 선형적으로 증가하며, 수익화가 시작되기도 전에 프로젝트를 파산 위기로 몰아넣을 수 있습니다.

1인 인디 개발자로서, 저는 로컬화된 아키텍처(localized architecture), 즉 주권적 엣지(sovereign edge)를 원했습니다.

이를 실현하기 위해서는 고도로 최적화된 런타임(runtime)과, 모바일 앱의 협소한 메모리 샌드박스(memory sandbox) 안에 들어갈 만큼 작으면서도, 의학적 조언을 환각(hallucinating)하지 않을 만큼 똑똑한 모델이 필요했습니다.

여기에 Google의 최신 LiteRT-LM 스택이 등장합니다.

익숙하지 않은 분들을 위해 설명하자면, LiteRT(이전의 TensorFlow Lite로 알려진 프로덕션 준비 완료 프레임워크)는 수백만 개의 엣지(edge) 애플리케이션이 신뢰하는 고성능 멀티 플랫폼 런타임입니다. 하지만 순수 LiteRT는 저수준 텐서 실행(low-level tensor execution)만을 처리합니다. LLM 앱을 구축하려면 그 상위에서 Key-Value (KV) 캐시를 관리하고, 프롬프트 템플릿(prompt templates)을 적용하며, 추측 디코딩(speculative decoding)을 처리하고, 함수 호출(function calling)을 실행할 오케스트레이션 레이어(orchestration layer)가 필요합니다. 그것이 바로 LiteRT-LM 개요가 제공하는 핵심 기능입니다.

그렇다면 두뇌는 무엇일까요? 바로 Gemma 4 E2B 모델 제품군입니다. 온디바이스(on-device) 애플리케이션을 위해 특별히 제작된 E2B 변체는 가벼우면서도 강력한 성능을 자랑합니다. 이 모델은 0.79GB의 가중치(weights)와 1.12GB의 임베딩 파라미터(embedding parameters)를 갖춘 텍스트 디코더를 약 2.59GB의 .litertlm 파일 하나에 담아냈습니다.

하지만 그 크기에 속지 마세요. 이 모델은 놀라운 **32k 컨텍스트 길이(context length)**를 지원하며, 별도의 설정 없이도 다중 토큰 예측(Multi-Token Prediction, MTP) 드래프터(drafter) 기능을 기본적으로 탑재하고 있습니다. 이를 통해 프레임워크는 모바일 칩에서 엄청난 속도 향상을 위해 여러 개의 다음 토큰을 동시에 예측할 수 있습니다.

제2장: 다리의 끝 (The Edge of the Bridge)

react-native-litert-lm과 같은 커뮤니티 래퍼(wrapper)들이 이러한 생태계를 연결하기 위해 등장했지만, 2.5GB에 달하는 가공되지 않은 모델 파일을 크로스 플랫폼(cross-platform) 환경에 그대로 집어넣는 것은 결코 플러그 앤 플레이(plug-and-play) 방식으로 해결되는 문제가 아닙니다. 성능 병목 현상을 진정으로 이해하고, 토큰 스트리밍(token-streaming) 속도를 최적화하며, 메모리 누수(memory leak)를 방지하려면 LiteRT-LM이 기기의 네이티브 메탈(native metal)에 어떻게 바인딩(bind)되는지 그 내부를 들여다봐야 합니다.

실제 구현은 다음과 같은 로우 네이티브(raw native) 개발 가이드를 따릅니다:

미션은 명확했습니다. LiteRT-LM 추론 엔진(inference engine)의 싱글톤 인스턴스(singleton instance)를 네이티브로 초기화하고, 무거운 .litertlm 모델 파일을 메모리에 한 번 로드하며, 브릿지(bridge)를 통해 스레드 안전한(thread-safe) sendMessage 메서드를 노출하고, 생성된 토큰들을 청크(chunk) 단위로 JavaScript UI에 다시 스트리밍하는 것이었습니다.

다음은 네이티브 엔진이 양쪽 플랫폼에 매핑되어 크로스 플랫폼 상태 계층(state layer)과 통신하는 구조적 로직입니다.

iOS 블루프린트 (Swift)

iOS에서는 컴파일러가 로컬 LiteRT-LM 프레임워크 헤더에 링크되어 모델을 초기화하고, RCTEventEmitter를 통해 비동기 스트림(asynchronous stream)을 관리합니다.

// WaterAIModule.swift
import Foundation
import LiteRTLM // 내부적인 네이티브 프레임워크 링크
...

Android 블루프린트 (Kotlin)

Android 측에서는 Java 네이티브 인터페이스(JNI) 오버헤드로 인해 컨텍스트(context)를 주의 깊게 처리해야 하며, 입력을 하위의 XNNPACK 또는 GPU 백엔드(backend)로 안전하게 전달해야 합니다.

// WaterAIModule.kt
package com.subraatakumar.watertracker

...

제3장: 607MB의 돌파구

다시 새벽 3시 15분으로 돌아가 봅시다. 코드는 완벽해 보였지만, 앱은 여전히 시작과 동시에 무너지고 있었습니다.

모바일 개발의 물리적 수학은 무자비합니다. 표준 모바일 애플리케이션은 OS에 의해 고위험 퇴거(eviction) 목록에 오르기 전까지 200MB에서 500MB 사이의 상주 RAM (Resident RAM)을 할당받습니다. 제 모델 파일 하나만 해도 2.59GB였습니다. 산을 어떻게 지갑 안에 구겨 넣을 수 있단 말입니까?

저는 Gemma 4 E2B의 기술 사양을 샅샅이 조사했습니다. 그때 런타임 메모리 매핑 (memory-mapping) 서브시스템의 메커니즘 속에 숨겨진 결정적인 단서를 발견했습니다.

LiteRT-LM은 매우 고도화된 메모리 점유율 (memory footprint) 최적화 전략을 구현합니다. 바로 모델 파라미터(model parameters)를 처리하는 방식을 분리하는 것입니다.

Gemma 4 E2B 모델 (총 2.59 GB 컨테이너)
├── 텍스트 디코더 가중치 (Text Decoder Weights, 0.79 GB)  ---> 상주 물리 RAM (Resident Physical RAM)에 엄격히 유지
└── 임베딩 파라미터 (Embedding Parameters, 1.12 GB)  ---> 디스크로부터 동적으로 메모리 매핑 (.mmap)
...

엔진은 2.59GB의 바이너리 전체를 물리 RAM 블록으로 통째로 복사하는 대신, 가중치 캐싱 메커니즘(XNNPACK의 네이티브 할당과 같은 방식)을 사용합니다. 핵심적인 0.79GB의 텍스트 디코더는 실행 메모리에 직접 고정(pin)하는 반면, 거대한 임베딩 레이어(embedding layers)는 기기의 저장 장치로부터 필요할 때마다 직접 메모리 매핑을 수행합니다.

물리적 메모리 점유율은 2.5GB에 도달하지 않습니다. 단지 약 607MB에서 700MB 사이에서 아름답게 안착합니다.

그런데 왜 제 빌드는 여전히 실패하고 있었을까요?

그것은 제가 모델 컨테이너를 패키징한 방식 때문이었습니다. 저는 원시 토크나이저(tokenizer) 설정 파일과 변환된 레이어들을 수동으로 함께 묶어버렸고, 이로 인해 엔진의 메모리 매핑 파서(parser)가 눈이 멀게 되었습니다. 파서는 전체 파일 레이아웃을 정렬되지 않고 매핑할 수 없는 원시 바이트 덩어리(raw byte-blobs)로 읽어 들였습니다.

이를 해결하기 위해 LiteRT-LM File Builder Documentation에 명시된 공식 직렬화(serialization) 아키텍처를 활용해야 했습니다. 빌더는 내부 헤더를 완벽하게 정렬하여 모바일 OS가 블록 오버헤드 없이 메모리 매핑을 실행할 수 있도록 합니다.

저는 Python 가상 환경을 실행하고 컴파일러 파이프라인 스크립트를 돌렸습니다:

pip install litert-lm-builder

litert-lm-builder \
...

스크립트는 파일 구조에 명시적인 정렬 오프셋(alignment offsets)을 주입하면서 바이너리 레이어(binary layers)를 패키징하며 깔끔하게 실행되었습니다. 저는 새로 생성된 최적화된 gemma-4-E2B-hydra.litertlm 파일을 앱 번들(app bundles)로 드래그했습니다.

그리고 다시 빌드(rebuild)를 눌렀습니다.

제4장: 듣고, 생각하고, 적응하다

컴파일이 완료되었습니다. 앱 화면의 채팅 아이콘을 탭했습니다. 인터페이스가 깔끔하고 미니멀한 프롬프트 박스로 전환되었습니다.

저는 다음과 같이 입력했습니다: "방금 85도(화씨)의 습한 날씨 속에서 5km 달리기를 마쳤습니다. 오늘 물을 500ml 마셨는데, 위험한 상태인가요?"

저는 두려운 네이티브 크래시 로그(native crash log)가 뜨기를 기다리며 숨을 죽였습니다.

모니터는 깨끗했습니다. 대신, 토큰(token) 하나하나가 유려한 속도로 모바일 뷰포트(viewport)에 쏟아지기 시작했습니다. 로컬 장치의 GPU 레이어(layers)에 의해 완전히 가속되어 거의 **초당 50 토큰(50 tokens per second)**에 달하는 속도였습니다.

[Thinking...]
열 조건(thermal conditions)을 고려할 때 사용자의 수분 상태가 위험할 정도로 낮습니다.
Prefill tokens: 1024 | Decode: ~52 tok/sec.
...

원격 서버에 핑(ping)을 보내지 않았습니다. 사용자 추적 데이터가 클라우드로 유출되지도 않았습니다. 그것은 손에 들린 유리 조각(핸드헬드 장치) 위에서 로컬로 실행되는, 완전히 안전하고 프라이빗하며 즉각적인 지능이었습니다.

핵심 요약: 2030년의 스택은 온디바이스(On-Device)다

엣지(edge)에서 지능을 구축하는 것은 클라우드 우선(cloud-first) 개발의 나태한 습관을 버리게 만듭니다. 실행 제한이 리튬 이온 배터리와 모바일 운영체제(OS) 커널에 의해 엄격하게 제한될 때는, 잘못된 알고리즘이나 비대해진 아키텍처 레이아웃에 무한한 탄력적 컴퓨팅 자원(elastic computing resources)을 쏟아부을 수 없기 때문입니다.

하지만 그 보상은 무엇일까요? 지속적인 유지 관리 비용 없이 사이드 프로젝트에 대해 진정한 애플리케이션 자율성을 얻는 것입니다.

이 아키텍처가 실제 제약 조건이 있는 프로덕션 환경에서 어떻게 작동하는지 경험하고 싶다면, 최신 두 생태계 모두에서 구현 내용을 라이브로 테스트해 볼 수 있습니다:

비싼 클라우드 엔드포인트 (Cloud endpoints)를 감싸는 단순한 래퍼 (Wrapper) 앱을 작성하던 시대는 저물고 있습니다. LiteRT-LM과 같은 네이티브 프레임워크 (Native frameworks)를 React Native와 같이 접근성이 높은 크로스 플랫폼 뷰포트 (Cross-platform viewports)로 연결함으로써, 우리는 단순히 앱을 출시하는 것을 넘어 고도로 최적화되고 독립적인 디지털 지능을 사용자들의 주머니 속에 직접 배포하고 있습니다.

그리고 가장 좋은 점은 무엇일까요? 다음에 제 클라우드 서버가 다운되더라도... 제 사용자들은 여전히 완벽하게 수분을 보충하고 있을 것이라는 점입니다.

기술 참조 장부 및 문서 (Technical Reference Ledger & Documentation)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0