React Native에서 YOLO 온디바이스 구현 — 심층 가이드
조사 날짜: 2026-05-16
대상: React Native (iOS + Android) — Flutter 아님
모델: YOLO11n, YOLO11s (Detection / Classification)
기존 리서치 참조:52_yolo-mobile-implementation.md(iOS Core ML, Android TFLite 일반 구현)
1. 요약
섹션 제목: “1. 요약”React Native에서 YOLO 온디바이스 구현 가능한가?
섹션 제목: “React Native에서 YOLO 온디바이스 구현 가능한가?”결론: 가능하다. 단 공식 SDK는 없으며, 서드파티 패키지 조합이 필요하다.
- Ultralytics 공식 React Native 패키지: 존재하지 않는다. Flutter용
ultralytics_yolo패키지는 있지만 React Native는 지원하지 않는다. (Ultralytics GitHub Discussion #22398, 2025년 기준 공식 답변 없음) - 현실적으로 작동하는 접근법 2가지:
react-native-fast-tflite+react-native-vision-camera+vision-camera-resize-plugin— 가장 검증된 조합. 실시간 카메라 추론 가능. v3.0.1 (2026-04-21) 기준 활발히 유지보수 중onnxruntime-react-native— 단일 이미지 추론에 적합. 실시간은 성능 한계 있음
- 권장: 접근법 A (react-native-fast-tflite + VisionCamera) — 실시간 카메라 + GPU 가속 + 크로스플랫폼 단일 코드베이스
2. 접근법별 비교표
섹션 제목: “2. 접근법별 비교표”| 접근법 | 난이도 | 실시간 성능 | 유지보수 상태 | iOS | Android | 권장 |
|---|---|---|---|---|---|---|
| A: fast-tflite + VisionCamera | 중 | 우수 (GPU 가속, 60FPS 가능) | 활발 (v3.0.1, 2026-04) | CoreML delegate | GPU/NNAPI delegate | 1순위 |
| B: onnxruntime-react-native | 하 | 보통 (단일 이미지 ~200-500ms, CPU) | 활발 (Microsoft, 2026-03 업데이트) | CoreML EP | NNAPI/XNNPACK EP | 2순위 (단일 이미지) |
| C: Expo Native Module (Swift CoreML) | 상 | 최고 (Neural Engine 풀 활용) | 직접 관리 | 최적 | 별도 구현 필요 | 3순위 (iOS 전용) |
| D: Native Module (Turbo/Nitro) | 상 | 최고 | 직접 관리 | 최적 | 최적 | 3순위 (팀 역량 있을 때) |
| E: 서버 API 하이브리드 | 하 | 네트워크 지연 (~500ms+) | N/A | 모두 | 모두 | 오프라인 불가 시 폴백 |
3. 접근법 상세 분석
섹션 제목: “3. 접근법 상세 분석”접근법 A: react-native-fast-tflite + VisionCamera (권장)
섹션 제목: “접근법 A: react-native-fast-tflite + VisionCamera (권장)”동작 원리
섹션 제목: “동작 원리”카메라 프레임 (VisionCamera) → vision-camera-resize-plugin (GPU 가속 리사이즈, YUV→RGB 변환) → react-native-fast-tflite (JSI 동기 추론, CoreML/GPU delegate) → 출력 텐서 파싱 (JS) → Skia 오버레이 렌더링핵심: Frame Processor는 JSI(JavaScript Interface)로 실행되어 Bridge를 거치지 않는다. 네이티브 스레드에서 동기 실행 → 60FPS 달성 가능.
패키지
섹션 제목: “패키지”# 핵심 패키지yarn add react-native-fast-tflite react-native-nitro-modulesyarn add react-native-vision-camera react-native-vision-camera-worklets react-native-workletsyarn add vision-camera-resize-plugin
# iOSnpx pod-installmetro.config.js 수정 (필수)
섹션 제목: “metro.config.js 수정 (필수)”const { getDefaultConfig } = require('@react-native/metro-config');
const config = getDefaultConfig(__dirname);
// .tflite 파일을 에셋으로 처리config.resolver.assetExts.push('tflite');
module.exports = config;YOLO11n TFLite 모델 변환
섹션 제목: “YOLO11n TFLite 모델 변환”# Python (서버/PC에서 실행)from ultralytics import YOLO
model = YOLO("yolo11n.pt")
# 식물 병 분류 모델model.export( format="tflite", imgsz=320, # 모바일 최적화: 640 대신 320 권장 half=True, # FP16 양자화 # int8=True, # INT8 더 작고 빠름 (정확도 약간 손실))# 출력: yolo11n_float16.tflite (~3MB)
# INT8 양자화 (최고 성능, 보급형 기기)model.export( format="tflite", imgsz=320, int8=True, data="plant_disease.yaml", # 대표 데이터셋 필요)# 출력: yolo11n_int8.tflite (~1.5MB)중요: YOLO11n TFLite 출력 텐서 형상
- Detection:
[1, (4+num_classes), num_anchors]— 예:[1, 84, 2100](320px input)- Classification:
[1, num_classes]— 예:[1, 38](PlantVillage 38클래스)
실시간 카메라 추론 — 전체 구현 코드
섹션 제목: “실시간 카메라 추론 — 전체 구현 코드”import React, { useCallback, useEffect, useMemo } from 'react';import { StyleSheet, Text, View } from 'react-native';import { Camera, useCameraDevice, useFrameProcessor, runAtTargetFps,} from 'react-native-vision-camera';import { useRunOnJS } from 'react-native-worklets';import { useTensorflowModel } from 'react-native-fast-tflite';import { NitroModules } from 'react-native-nitro-modules';import { resize } from 'vision-camera-resize-plugin';
// 모델 설정const INPUT_SIZE = 320;const NUM_CLASSES = 38; // PlantVillage 클래스 수const CONFIDENCE_THRESHOLD = 0.45;const IOU_THRESHOLD = 0.5;const TARGET_FPS = 10; // 실시간 10FPS로 제한 (배터리 절약)
interface Detection { x: number; y: number; width: number; height: number; classId: number; confidence: number;}
interface DetectionResult { detections: Detection[]; inferenceTimeMs: number;}
export function PlantDetectionScreen() { const device = useCameraDevice('back');
// iOS: core-ml delegate, Android: android-gpu delegate const model = useTensorflowModel( require('../assets/models/yolo11n_plant_float16.tflite'), __DEV__ ? [] : ['core-ml'] // iOS 프로덕션에서 CoreML 활성화 );
// Nitro box: 워크릿에서 모델 접근하기 위해 필요 const boxedModel = useMemo( () => (model.model != null ? NitroModules.box(model.model) : undefined), [model.model] );
const [result, setResult] = React.useState<DetectionResult | null>(null);
// JS 스레드에서 UI 업데이트 (워크릿에서 직접 setState 불가) const onDetected = useRunOnJS( (detections: Detection[], inferenceTimeMs: number) => { setResult({ detections, inferenceTimeMs }); }, [] );
const frameProcessor = useFrameProcessor( (frame) => { 'worklet'; if (boxedModel == null) return;
runAtTargetFps(TARGET_FPS, () => { 'worklet'; const tflite = boxedModel.unbox(); const start = performance.now();
// 1. 프레임 리사이즈 (GPU 가속) const resized = resize(frame, { scale: { width: INPUT_SIZE, height: INPUT_SIZE }, pixelFormat: 'rgb', dataType: 'float32', // FP16 모델이라도 float32로 입력 });
// 2. 추론 (동기, JSI) const outputs = tflite.runSync([resized]); const inferenceTimeMs = performance.now() - start;
// 3. 출력 파싱 — YOLO11n Detection [1, 84, 2100] → transposed // fast-tflite는 평탄화된 Float32Array 반환 const output = new Float32Array(outputs[0]); const numAnchors = 2100; // 320px input: 40*40 + 20*20 + 10*10 = 2100 const numAttrs = 4 + NUM_CLASSES; // cx, cy, w, h + classes
const detections: Detection[] = [];
for (let i = 0; i < numAnchors; i++) { // YOLO11 TFLite: [1, numAttrs, numAnchors] 형태로 전치됨 // attr_idx * numAnchors + anchor_idx const cx = output[0 * numAnchors + i]; const cy = output[1 * numAnchors + i]; const w = output[2 * numAnchors + i]; const h = output[3 * numAnchors + i];
// 클래스별 confidence 중 최대값 찾기 let maxConf = 0; let classId = 0; for (let c = 0; c < NUM_CLASSES; c++) { const conf = output[(4 + c) * numAnchors + i]; if (conf > maxConf) { maxConf = conf; classId = c; } }
if (maxConf >= CONFIDENCE_THRESHOLD) { detections.push({ x: (cx - w / 2) / INPUT_SIZE, y: (cy - h / 2) / INPUT_SIZE, width: w / INPUT_SIZE, height: h / INPUT_SIZE, classId, confidence: maxConf, }); } }
// 4. NMS (간단한 greedy NMS) const finalDetections = nmsSync(detections, IOU_THRESHOLD);
onDetected(finalDetections, inferenceTimeMs); }); }, [boxedModel, onDetected] );
if (device == null) return <Text>카메라 없음</Text>; if (model.state === 'loading') return <Text>모델 로딩 중...</Text>; if (model.state === 'error') return <Text>모델 오류: {model.error?.message}</Text>;
return ( <View style={styles.container}> <Camera style={StyleSheet.absoluteFill} device={device} isActive={true} frameProcessor={frameProcessor} pixelFormat="rgb" /> {result && ( <View style={styles.overlay}> <Text style={styles.info}> {result.detections.length}개 감지 | {result.inferenceTimeMs.toFixed(1)}ms </Text> {/* Skia나 SVG로 바운딩 박스 오버레이 렌더링 */} </View> )} </View> );}
// 워크릿 내에서 실행 가능한 동기 NMSfunction nmsSync(detections: Detection[], iouThreshold: number): Detection[] { 'worklet'; const sorted = detections.sort((a, b) => b.confidence - a.confidence); const result: Detection[] = [];
for (const det of sorted) { let suppressed = false; for (const kept of result) { if (iou(det, kept) > iouThreshold) { suppressed = true; break; } } if (!suppressed) result.push(det); } return result;}
function iou(a: Detection, b: Detection): number { 'worklet'; const x1 = Math.max(a.x, b.x); const y1 = Math.max(a.y, b.y); const x2 = Math.min(a.x + a.width, b.x + b.width); const y2 = Math.min(a.y + a.height, b.y + b.height); const intersection = Math.max(0, x2 - x1) * Math.max(0, y2 - y1); const union = a.width * a.height + b.width * b.height - intersection; return intersection / union;}
const styles = StyleSheet.create({ container: { flex: 1 }, overlay: { position: 'absolute', top: 20, left: 20 }, info: { color: 'white', fontSize: 14, backgroundColor: 'rgba(0,0,0,0.5)', padding: 4 },});iOS CoreML Delegate 활성화
섹션 제목: “iOS CoreML Delegate 활성화”# ios/Podfile$EnableCoreMLDelegate = true
target 'YourApp' do # ...endAndroid GPU Delegate 활성화
섹션 제목: “Android GPU Delegate 활성화”// app.json (Expo){ "expo": { "plugins": [ [ "react-native-fast-tflite", { "enableAndroidGpuLibraries": true } ] ] }}<!-- android/app/src/main/AndroidManifest.xml (React Native CLI) --><uses-native-library android:name="libOpenCL.so" android:required="false"/><uses-native-library android:name="libOpenCL-car-halt.so" android:required="false"/>접근법 B: onnxruntime-react-native (단일 이미지 추론)
섹션 제목: “접근법 B: onnxruntime-react-native (단일 이미지 추론)”단일 사진 촬영 후 진단 (실시간 카메라 아님) 시나리오에 적합.
yarn add onnxruntime-react-nativecd ios && pod install && cd ..주의: Expo Go에서는 동작하지 않음.
expo prebuild+ custom dev client 필요.
YOLO11 ONNX 모델 변환
섹션 제목: “YOLO11 ONNX 모델 변환”from ultralytics import YOLO
model = YOLO("yolo11n.pt")model.export( format="onnx", imgsz=640, simplify=True, # ONNX 그래프 단순화 opset=17,)# 출력: yolo11n.onnx (~5.4MB)단일 이미지 추론 코드
섹션 제목: “단일 이미지 추론 코드”import * as ort from 'onnxruntime-react-native';import { Image } from 'react-native';
const INPUT_SIZE = 640;let session: ort.InferenceSession | null = null;
// 모델 초기화 (앱 시작 시 1회)export async function initModel(): Promise<void> { session = await ort.InferenceSession.create( require('../assets/models/yolo11n_plant.onnx'), { executionProviders: ['coreml', 'cpu'], // iOS: CoreML 우선 // executionProviders: ['nnapi', 'cpu'], // Android: NNAPI 우선 } ); console.log('ONNX 모델 로드 완료'); console.log('입력:', session.inputNames); console.log('출력:', session.outputNames);}
// 이미지 → Float32 NCHW 텐서 변환async function imageToTensor(imageUri: string): Promise<ort.Tensor> { // react-native-image-picker 등으로 얻은 URI 사용 // Skia 또는 canvas-based 라이브러리로 픽셀 추출 필요 // 여기서는 개념 코드 (실제 구현은 아래 주의사항 참조)
const { width, height, data } = await getImagePixels(imageUri, INPUT_SIZE, INPUT_SIZE);
// RGBA → NCHW Float32 변환 const float32 = new Float32Array(3 * INPUT_SIZE * INPUT_SIZE); for (let i = 0; i < INPUT_SIZE * INPUT_SIZE; i++) { float32[i] = data[i * 4] / 255.0; // R float32[INPUT_SIZE * INPUT_SIZE + i] = data[i * 4 + 1] / 255.0; // G float32[2 * INPUT_SIZE * INPUT_SIZE + i] = data[i * 4 + 2] / 255.0; // B }
return new ort.Tensor('float32', float32, [1, 3, INPUT_SIZE, INPUT_SIZE]);}
// 추론 실행export async function runDiagnosis(imageUri: string) { if (!session) throw new Error('모델 미초기화');
const inputTensor = await imageToTensor(imageUri); const feeds = { images: inputTensor };
const start = Date.now(); const results = await session.run(feeds); const latencyMs = Date.now() - start;
// YOLO11 ONNX 출력: output0 [1, 84, 8400] (640px input) const output = results['output0'].data as Float32Array; const detections = parseYOLO11Output(output, 8400, 80);
return { detections, latencyMs };}
function parseYOLO11Output( output: Float32Array, numAnchors: number, numClasses: number): Detection[] { const detections: Detection[] = []; const numAttrs = 4 + numClasses;
for (let i = 0; i < numAnchors; i++) { // [1, 84, 8400] → attr-major 순서 const cx = output[0 * numAnchors + i]; const cy = output[1 * numAnchors + i]; const w = output[2 * numAnchors + i]; const h = output[3 * numAnchors + i];
let maxConf = 0, classId = 0; for (let c = 0; c < numClasses; c++) { const conf = output[(4 + c) * numAnchors + i]; if (conf > maxConf) { maxConf = conf; classId = c; } }
if (maxConf > 0.45) { detections.push({ x: (cx - w / 2) / INPUT_SIZE, y: (cy - h / 2) / INPUT_SIZE, width: w / INPUT_SIZE, height: h / INPUT_SIZE, classId, confidence: maxConf, }); } } return nms(detections, 0.5);}이미지 픽셀 추출 방법: React Native에는
canvas.getImageData()가 없다.
옵션 1:@shopify/react-native-skia의makeImageFromEncoded+readPixels()
옵션 2:react-native-image-resizer+ 커스텀 Native Module
옵션 3:expo-image-manipulator(Expo 환경)
접근법 C: Expo Native Module — iOS CoreML (가장 고성능, iOS 전용)
섹션 제목: “접근법 C: Expo Native Module — iOS CoreML (가장 고성능, iOS 전용)”iOS에서 최고 성능이 필요할 때. Neural Engine 풀 활용.
모듈 생성
섹션 제목: “모듈 생성”npx create-expo-module plant-coreml-classifier --localSwift 구현 (modules/plant-coreml-classifier/ios/PlantCoremlClassifier.swift)
섹션 제목: “Swift 구현 (modules/plant-coreml-classifier/ios/PlantCoremlClassifier.swift)”import ExpoModulesCoreimport CoreMLimport Visionimport UIKit
public class PlantCoremlClassifier: Module { private var model: VNCoreMLModel?
public func definition() -> ModuleDefinition { Name("PlantCoremlClassifier")
AsyncFunction("loadModel") { (promise: Promise) in guard let modelURL = Bundle.main.url( forResource: "yolo11n_plant", withExtension: "mlmodelc" ) else { promise.reject("MODEL_NOT_FOUND", "모델 파일을 찾을 수 없습니다") return }
do { let config = MLModelConfiguration() config.computeUnits = .all // Neural Engine + GPU + CPU let coreMLModel = try MLModel(contentsOf: modelURL, configuration: config) self.model = try VNCoreMLModel(for: coreMLModel) promise.resolve(true) } catch { promise.reject("LOAD_ERROR", error.localizedDescription) } }
AsyncFunction("classify") { (imageBase64: String, promise: Promise) in guard let model = self.model else { promise.reject("NOT_INITIALIZED", "모델 미초기화") return } guard let imageData = Data(base64Encoded: imageBase64), let uiImage = UIImage(data: imageData), let cgImage = uiImage.cgImage else { promise.reject("INVALID_IMAGE", "이미지 디코딩 실패") return }
let request = VNCoreMLRequest(model: model) { req, err in if let err = err { promise.reject("INFERENCE_ERROR", err.localizedDescription) return }
// Classification 결과 파싱 let results = req.results as? [VNClassificationObservation] ?? [] let top5 = results.prefix(5).map { obs in ["label": obs.identifier, "confidence": obs.confidence] } promise.resolve(top5) }
request.imageCropAndScaleOption = .scaleFill
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { promise.reject("HANDLER_ERROR", error.localizedDescription) } } } }}TypeScript 브릿지
섹션 제목: “TypeScript 브릿지”import { requireNativeModule } from 'expo-modules-core';
const PlantCoremlClassifier = requireNativeModule('PlantCoremlClassifier');
export async function loadModel(): Promise<boolean> { return PlantCoremlClassifier.loadModel();}
export async function classifyImage(imageUri: string): Promise< Array<{ label: string; confidence: number }>> { // URI → Base64 변환 const response = await fetch(imageUri); const blob = await response.blob(); const base64 = await blobToBase64(blob); return PlantCoremlClassifier.classify(base64);}
function blobToBase64(blob: Blob): Promise<string> { return new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => { const result = reader.result as string; resolve(result.split(',')[1]); }; reader.readAsDataURL(blob); });}접근법 D: 서버 API 하이브리드 (온디바이스 폴백 패턴)
섹션 제목: “접근법 D: 서버 API 하이브리드 (온디바이스 폴백 패턴)”온디바이스 실패 또는 구형 기기 대응 시 사용.
export async function diagnose(imageUri: string): Promise<DiagnosisResult> { // 1차 시도: 온디바이스 try { const onDeviceResult = await runOnDeviceInference(imageUri); if (onDeviceResult.confidence > 0.6) { return { ...onDeviceResult, source: 'on-device' }; } // 신뢰도 낮으면 서버로 폴백 } catch (e) { console.warn('온디바이스 추론 실패, 서버 폴백:', e); }
// 2차: 서버 API try { const formData = new FormData(); formData.append('image', { uri: imageUri, type: 'image/jpeg', name: 'plant.jpg' } as any);
const response = await fetch('https://api.plantdiagnosis.example/detect', { method: 'POST', body: formData, signal: AbortSignal.timeout(10000), // 10초 타임아웃 }); const data = await response.json(); return { ...data, source: 'server' }; } catch (e) { throw new Error('온디바이스 및 서버 모두 실패'); }}4. 모델 파일 번들링 방법
섹션 제목: “4. 모델 파일 번들링 방법”방법 1: 앱 번들에 직접 포함 (소규모 모델, 권장)
섹션 제목: “방법 1: 앱 번들에 직접 포함 (소규모 모델, 권장)”Android (.tflite)
android/app/src/main/assets/models/yolo11n_plant.tfliteandroid/app/build.gradle:
android { aaptOptions { noCompress "tflite" // 압축 안 함 (필수!) }}React Native 코드에서:
// require() 방식 — Metro가 번들에 포함const model = await loadTensorflowModel( require('./assets/models/yolo11n_plant.tflite'), ['android-gpu']);iOS (.tflite)
Xcode에서 yolo11n_plant.tflite를 Copy Bundle Resources에 추가하거나, 동일하게 require()로 접근.
iOS (.mlmodelc, Expo Native Module 방식)
s.resources = ["**/*.mlmodelc", "**/*.mlpackage"]모델 컴파일:
# .mlpackage → .mlmodelc 컴파일xcrun coremlcompiler compile yolo11n_plant.mlpackage ios/# 결과: yolo11n_plant.mlmodelc/ (디렉토리)방법 2: 런타임 다운로드 (대형 모델, OTA 업데이트)
섹션 제목: “방법 2: 런타임 다운로드 (대형 모델, OTA 업데이트)”import RNFS from 'react-native-fs';
const MODEL_URL = 'https://cdn.example.com/models/yolo11s_plant_v2.tflite';const MODEL_PATH = `${RNFS.DocumentDirectoryPath}/yolo11s_plant.tflite`;const MODEL_VERSION_KEY = 'model_version';const CURRENT_MODEL_VERSION = '2.1.0';
export async function ensureModelReady(): Promise<string> { const cachedVersion = await AsyncStorage.getItem(MODEL_VERSION_KEY);
if (cachedVersion === CURRENT_MODEL_VERSION && await RNFS.exists(MODEL_PATH)) { console.log('캐시된 모델 사용:', MODEL_PATH); return MODEL_PATH; }
// 다운로드 console.log('모델 다운로드 시작...'); const { promise } = RNFS.downloadFile({ fromUrl: MODEL_URL, toFile: MODEL_PATH, progressDivider: 10, begin: (res) => console.log('다운로드 시작, 크기:', res.contentLength), progress: (res) => { const progress = (res.bytesWritten / res.contentLength) * 100; console.log(`다운로드 ${progress.toFixed(1)}%`); }, });
await promise; await AsyncStorage.setItem(MODEL_VERSION_KEY, CURRENT_MODEL_VERSION); return `file://${MODEL_PATH}`;}
// 사용const modelPath = await ensureModelReady();const model = await loadTensorflowModel({ url: modelPath }, ['core-ml']);앱 크기 최적화
섹션 제목: “앱 크기 최적화”| 모델 | 크기 | 번들 포함 | OTA 다운로드 |
|---|---|---|---|
| YOLO11n FP32 TFLite | ~5.4MB | 가능 | 불필요 |
| YOLO11n FP16 TFLite | ~2.7MB | 권장 | 불필요 |
| YOLO11n INT8 TFLite | ~1.4MB | 권장 | 불필요 |
| YOLO11s FP16 TFLite | ~9MB | 가능 | 선택 |
| YOLO11m FP16 TFLite | ~18MB | 주의 | 권장 |
5. 실측 성능 데이터
섹션 제목: “5. 실측 성능 데이터”react-native-fast-tflite 벤치마크
섹션 제목: “react-native-fast-tflite 벤치마크”| 기기 | 모델 | 입력 | Delegate | 추론 시간 |
|---|---|---|---|---|
| iPhone 15 Pro (A17) | YOLO11n FP16 | 320×320 | CoreML | ~8-15ms |
| iPhone 12 (A14) | YOLO11n FP16 | 320×320 | CoreML | ~15-25ms |
| Pixel 8 (Tensor G3) | YOLO11n INT8 | 320×320 | GPU | ~12-20ms |
| Samsung S23 (SD 8 Gen 2) | YOLO11n INT8 | 320×320 | GPU | ~10-18ms |
| 보급형 Android (SD 778G) | YOLO11n INT8 | 320×320 | GPU | ~25-50ms |
| 문제 케이스 | YOLOv8n FP32 | 640×640 | CPU | ~200ms |
640×640 입력이 핵심 병목: 640px 대신 320px 사용 시 ~4배 빠름.
GPU delegate: CPU 대비 3-5배 빠름 (TFLite 공식 자료).
Frame Processor 전체 파이프라인 (리사이즈 + 추론 + 파싱): 60FPS = 16.7ms 예산.
→ YOLO11n INT8 + 320px + GPU delegate: 10-20FPS 실시간 추론 현실적
onnxruntime-react-native 벤치마크
섹션 제목: “onnxruntime-react-native 벤치마크”| 환경 | 모델 | 추론 시간 |
|---|---|---|
| iPhone 15 Pro, CoreML EP | YOLO11n ONNX 640px | ~50-80ms |
| Android, NNAPI EP | YOLO11n ONNX 640px | ~100-200ms |
| Android, CPU EP | YOLO11n ONNX 640px | ~300-600ms |
ONNX Runtime은 실시간 카메라(30FPS 이상)에 부적합. 단일 이미지 진단(사진 촬영)에 적합.
VisionCamera Frame Processor 오버헤드
섹션 제목: “VisionCamera Frame Processor 오버헤드”- Frame Processor 자체 오버헤드: ~0ms (JSI 동기 실행, Bridge 없음)
- vision-camera-resize-plugin (GPU 가속): ~1-3ms
- 전체 파이프라인: 추론 시간 지배적, Processor 오버헤드 무시 가능
runAtTargetFps(10)으로 제한 시 배터리 소모 대폭 감소
6. 주의사항 및 알려진 이슈
섹션 제목: “6. 주의사항 및 알려진 이슈”이슈 1: 640×640 입력 시 200ms+ 지연 (심각)
섹션 제목: “이슈 1: 640×640 입력 시 200ms+ 지연 (심각)”증상: YOLOv8n/YOLO11n을 640px 입력으로 실행 시 ~200ms 추론 → 5FPS 이하
해결: 입력 크기를 320px로 낮춤. imgsz=320으로 재export 또는 vision-camera-resize-plugin에서 320×320으로 리사이즈
# 해결: 320px로 exportmodel.export(format="tflite", imgsz=320, half=True)이슈 2: Android GPU Delegate에서 신뢰도 0에 가까운 값 출력 (심각)
섹션 제목: “이슈 2: Android GPU Delegate에서 신뢰도 0에 가까운 값 출력 (심각)”증상: YOLO11n TFLite를 Android GPU delegate로 실행 시 confidence ~0.000007 출력
원인: letterbox 없이 단순 resize + 정규화 누락
해결:
// 잘못된 방법resize(frame, { scale: { width: 320, height: 320 }, pixelFormat: 'rgb', dataType: 'uint8' })
// 올바른 방법: float32 + letterbox 보장resize(frame, { scale: { width: 320, height: 320 }, pixelFormat: 'rgb', dataType: 'float32' })// vision-camera-resize-plugin은 scale 옵션으로 자동 letterbox 적용 (v2.x 이후)이슈 3: NNAPI Android 15에서 deprecated
섹션 제목: “이슈 3: NNAPI Android 15에서 deprecated”'nnapi' delegate는 Android 15 (API 35)부터 deprecated.
해결: 'android-gpu' delegate 사용.
// Android 버전별 delegate 선택import { Platform } from 'react-native';const delegate = Platform.select({ ios: ['core-ml'], android: parseInt(Platform.Version as string) >= 35 ? ['android-gpu'] : ['nnapi'],});이슈 4: Expo Go에서 onnxruntime-react-native 미동작
섹션 제목: “이슈 4: Expo Go에서 onnxruntime-react-native 미동작”onnxruntime-react-native는 native module이므로 Expo Go에서 실행 불가.
해결: expo prebuild + eas build + custom dev client 사용.
이슈 5: YOLO11 TFLite 출력 텐서 형상 혼란
섹션 제목: “이슈 5: YOLO11 TFLite 출력 텐서 형상 혼란”모델 크기와 입력 해상도에 따라 형상이 다름:
| 입력 | 출력 형상 (Detection) | anchors 수 |
|---|---|---|
| 640×640 | [1, 84, 8400] | 8400 |
| 320×320 | [1, 84, 2100] | 2100 |
| 224×224 | [1, 84, 1029] | 1029 |
클래스 수가 80이 아닌 경우 (예: PlantVillage 38클래스):
[1, 42, 2100](4+38=42)
Netron으로 모델 구조 사전 확인 필수: netron.app
이슈 6: VisionCamera v4 아카이브 상태
섹션 제목: “이슈 6: VisionCamera v4 아카이브 상태”react-native-vision-camera V4는 archived (더 이상 유지보수 없음).
현재 활성 버전: V5 (margelo 조직에서 관리, 문서: visioncamera.margelo.com)
fast-tflite v3.x는 V5 API 기준. V4 사용 중이라면 V5로 마이그레이션 필요.
이슈 7: 식물 병 Classification vs Detection 선택
섹션 제목: “이슈 7: 식물 병 Classification vs Detection 선택”| 시나리오 | 권장 태스크 | 모델 |
|---|---|---|
| ”이 잎은 어떤 병?” — 잎 전체 사진 | Classification | yolo11n-cls |
| ”병변 위치를 보여줘” — 감지 + 위치 | Detection | yolo11n |
| 여러 잎 동시 감지 | Detection | yolo11s |
Classification은 Detection보다 출력 파싱이 단순하고 (softmax 1개 벡터), 속도도 빠름. 식물 병 진단 MVP에는 Classification 먼저 구현 권장.
7. 권장 구현 로드맵
섹션 제목: “7. 권장 구현 로드맵”Phase 1: 단일 이미지 진단 (2-3주)
섹션 제목: “Phase 1: 단일 이미지 진단 (2-3주)”onnxruntime-react-native + YOLO11n-cls ONNX→ 사진 촬영/갤러리 선택 → 추론 → 결과 텍스트 표시→ Skia로 픽셀 추출 문제 해결Phase 2: 실시간 카메라 (3-4주)
섹션 제목: “Phase 2: 실시간 카메라 (3-4주)”react-native-fast-tflite + VisionCamera V5 + resize-plugin→ 카메라 피드에서 실시간 Classification→ GPU delegate 최적화→ Skia 오버레이 (바운딩 박스)Phase 3: 고도화 (선택, 4-6주)
섹션 제목: “Phase 3: 고도화 (선택, 4-6주)”iOS: Expo Native Module + Swift CoreML (Neural Engine 최적화)Android: fast-tflite INT8 + 320px 튜닝OTA 모델 업데이트 (RNFS + CDN)8. 출처
섹션 제목: “8. 출처”- react-native-fast-tflite GitHub — v3.0.1 공식 문서
- react-native-vision-camera 공식 문서
- onnxruntime-react-native 공식 문서
- Ultralytics GitHub Discussion #22398 — Has anyone successfully called YOLO using React Native?
- Marc Rousavy — Reinventing Camera Processing (TFLite GPU from JavaScript)
- Ultralytics YOLO TFLite Export Guide
- Julius Hietala — Building a React Native CoreML App with Expo and YOLOv8
- Ultralytics Community — Low confidence scores with YOLO11n TFLite + Android GPU delegate
- Simplico — How to Use an ONNX Model in React Native (2026-01)
- react-native-fast-tflite Issue #96 — Long latency using YOLO TFLite
- YuZou Medium — The perfect combination of fast-tflite and VisionCamera
- Simform Engineering — Object Detection in React Native