지난 1부에서는 RayNeo X2 개발 환경을 구축하고, 양안 디스플레이를 위한 카메라 프리뷰를 띄우는 것까지 성공적으로 마쳤어요. 멋진 듀얼 스크린 프리뷰를 눈으로 확인하셨다면 이제 정말 중요한 단계로 넘어가야 합니다. 바로 **사용자의 움직임을 앱과 연결하는 일**입니다. 이번 2부에서는 RayNeo X2의 핵심 입력 방식인 **안경 다리 터치(Temple Action)**를 감지해서 사진을 찍고 갤러리에 저장하는 방법을 구체적인 코드 흐름과 함께 살펴볼게요. 또한 개발 과정에서 제가 마주했던 **RayNeo 플랫폼의 실질적인 제약 사항들**과 이를 어떻게 해결했는지에 대한 현실적인 개발 경험도 공유드릴게요. <br> <br> # 1. Temple Action 감지 및 제스처 처리하기 일반적인 스마트폰 앱에서는 화면을 직접 터치(2D 입력)하여 상호작용하지만, RayNeo X2는 안경 다리 터치나 링 컨트롤러를 사용하는 **1차원 입력**에 의존해요. 이 제한적인 입력을 사용자가 원하는 동작(예: 사진 찍기)으로 변환하는 것이 바로 **Temple Action**의 역할입니다. <br> ## 1.1 1차원 입력 변환하기 RayNeo ARDK는 원시적인 `MotionEvent` 데이터를 **스와이프, 탭, 더블 탭**과 같은 의미 있는 제스처 기본 요소로 해석합니다. 이러한 제스처는 `TempleAction`이라는 구조화된 이벤트 객체로 캡슐화되어 처리됩니다. 우리가 할 일은 ARDK가 제공하는 `TempleActionViewModel`을 활용하여 이러한 제스처 이벤트가 발생했을 때 원하는 기능을 실행하도록 연결하는 거예요. 이때 **Kotlin 코루틴**과 `lifecycleScope`를 활용하면, 액티비티가 활성화된 상태(`Lifecycle.State.RESUMED`)일 때만 이벤트를 안전하게 처리할 수 있어 효율적입니다 <br> **✅ 코루틴을 활용한 이벤트 수신:** ```Kotlin private fun observeTempleAction() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { // 앱이 활성 상태일 때만 이벤트를 수신 templeActionViewModel.state.collect { action -> when (action) { is TempleAction.Click -> takePhoto() // 가볍게 터치하면 촬영 함수 호출 is TempleAction.DoubleClick -> finish() // 더블 터치하면 앱 종료 else -> { /* Ignore other events */ } // 다른 이벤트는 무시 } } } } } ``` 이 코드는 안경 다리를 **가볍게 터치(Click)**하면 미리 정의한 `takePhoto()` 함수를 실행하고, **두 번 터치(DoubleClick)**하면 현재 액티비티를 종료하는 기능을 구현합니다. <br><br><br> # 2. CameraX로 이미지 캡처 및 저장하기 Temple Action을 통해 사진 찍기 명령(`takePhoto()`)이 들어왔다면, 이제 카메라 기능을 실행할 차례입니다. 저는 카메라 통합 작업을 간소화해주는 Jetpack 라이브러리인 **CameraX**를 활용하여 이 기능을 구현했습니다. 사진을 찍기 전, 1부에서 다루었던 카메라 프리뷰 설정을 다시 상기해 보세요. RayNeo X2는 하나의 물리적 카메라를 가지고 있지만, **양안(Binocular) 디스플레이**에 프리뷰를 렌더링하기 위해 `BaseMirrorActivity`나 `BindingPair` 구조를 사용하고, CameraX의 `Preview` 객체 두 개를 생성하여 좌우 뷰에 연결해야 했습니다. <br> ## 2.1 MediaStore API를 이용한 사진 저장 사진을 캡처한 후에는 안드로이드 시스템에서 표준화된 방식인 **MediaStore API**를 사용하여 파일을 저장해야 합니다. 이 과정을 통해 캡처된 이미지가 갤러리나 다른 미디어 앱에서 인식될 수 있습니다. <br> 아래 **takePhoto()** **함수**에서는 파일 이름 생성, 저장 경로 설정(`ContentValues` 사용), 그리고 캡처 실행을 담당합니다. ```Kotlin private fun takePhoto() { val imageCapture = imageCapture ?: return // imageCapture 객체가 없다면 함수 종료 // 1. 파일 이름 및 메타데이터 정의 val name = "IMG_" + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, "IMG_$name.jpg") put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") // Android Q(10) 이상에서는 RELATIVE_PATH를 사용하여 전용 폴더에 저장 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/TestApp") } } // 2. 저장 옵션 설정 (MediaStore 경로 지정) val outputOptions = ImageCapture.OutputFileOptions .Builder(contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) .build() // 3. 사진 캡처 실행 및 콜백 처리 imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(output: ImageCapture.OutputFileResults) { // 저장 성공 시 사용자에게 피드백 제공 (FToast는 ARDK의 듀얼 디스플레이 Toast 컴포넌트) FToast.show("Photo has been saved to the gallery.") } override fun onError(exc: ImageCaptureException) { FToast.show("Failed to save photo") } } ) } ``` <br> 1. **메타데이터 정의:** 먼저 현재 시간 정보를 사용해 파일명(`IMG_yyyyMMdd_HHmmss.jpg`)을 만들고, `ContentValues`를 사용하여 파일의 메타데이터(파일명, MIME 타입, 저장 경로)를 설정합니다. 2. **경로 지정:** 안드로이드 10 이상에서는 사진을 `Pictures/App` 또는 우리가 지정한 `Pictures/TestApp`과 같은 전용 폴더에 저장할 수 있도록 `MediaStore.MediaColumns.RELATIVE_PATH`를 설정합니다. 3. **캡처 실행:** `ImageCapture.takePicture()` 메서드를 호출하면 사진이 캡처되고, `OnImageSavedCallback`을 통해 저장 성공/실패 여부를 사용자에게 `FToast`로 알려줍니다. <br> 이 과정은 캡처한 사진이 시스템 갤러리에 자동으로 등록되어 다른 앱에서도 접근하고 볼 수 있도록 보장하는 표준화된 방법이에요. <br><br><br> # 3. RayNeo X2 플랫폼의 한계와 해결책 RayNeo X2는 **독립적인 Android OS**와 **공식 SDK**를 제공하여 높은 개발 자유도와 확장성을 제공합니다. 이는 맞춤형 보조 애플리케이션 개발에 가장 유망한 플랫폼으로 평가받은 이유이기도 합니다. 하지만 실제 개발 및 테스트 과정에서 일반적인 안드로이드 개발 환경과 비교되는 몇 가지 현실적인 제약 사항들을 발견했어요. 이러한 한계점을 이해하고 적절히 대응하는 것이 필요합니다. <br> ## 3.1 불안정한 음성 인식 RayNeo X2의 내장 마이크 성능은 특히 **시끄러운 환경**에서 음성 인식 정확도가 낮아 신뢰할 수 없는 수준이었습니다. 핸즈프리 조작이나 접근성 기능을 음성 명령에 의존하는 경우, **잦은 인식 오류**로 인해 사용자가 명령을 여러 번 반복해야 했고, 이는 전반적인 사용성을 크게 떨어뜨렸습니다. <br> **✅ 해결책** 음성 상호작용의 신뢰성이 확보되지 않는다면, **물리적 버튼이나 안경 다리 터치(Temple Action)**와 같은 멀티모달 입력 방식을 주요 제어 수단으로 준비하는 것이 필수적입니다. <br><br> ## 3.2 TTS 엔진 부재, 접근성의 장벽 RayNeo X2는 **내장 TTS(Text-to-Speech) 엔진이 없어요**. TTS는 시각 장애가 있는 사용자나 읽기에 어려움을 겪는 사용자에게 음성 피드백을 제공하는 데 필수적인 기술입니다. 기본적으로 음성 안내를 받을 수 없어 접근성에 큰 장벽이 되며, 앱 생태계가 폐쇄적이기 때문에 사용자가 별도의 TTS 엔진 APK를 수동으로 설치하는 것이 직관적이지 않고 번거롭습니다. <br> **✅ 해결책** 저는 이 문제를 해결하기 위해 세 가지 방법을 탐색했습니다(녹음 파일, 오프라인 라이브러리, 온라인 API). 만약 앱이 "사진이 저장되었습니다"와 같이 **미리 정해진, 고정된 안내 구문**만 제공하면 된다면, `MediaPlayer`를 통한 녹음 파일 재생이 **현재 RayNeo X2 환경에서 가장 안정적이고 확실한 최선의 선택**입니다. 그러나 녹음 파일 방식의 단점은 **동적인 문장이나 실시간 데이터를 음성으로 제공할 수 없다**는 점입니다. TTS의 본래 목적인 복잡한 정보 전달이 불가능하죠. 따라서, **TTS가 제공하지 못하는 기능을 다른 경로로 대체**하는 것이 장기적인 최선책입니다. 만약 동적 음성 피드백이 반드시 필요하다면, **오픈 소스 라이브러리를 앱에 번들링**하거나, 사용자가 TTS 엔진을 별도로 설치하도록 **명확한 안내**를 제공하는 보완 전략을 준비해야 합니다. 이는 앱 용량 및 개발 복잡성을 증가시키지만, 동적 피드백을 구현할 수 있는 유일한 경로입니다. <br><br> ## 3.3 기본 갤러리 앱의 부재 RayNeo X2에는 **기본 갤러리 또는 사진 뷰어 애플리케이션이 없습니다**. 사진을 찍더라도 기기에서 바로 이미지를 확인할 수 없어요. 사진을 보려면 별도의 서드파티 파일 관리자 앱(예: Google Files)을 설치하거나, 기기를 스마트폰에 연결하여 RayNeo 서버를 통해 파일을 다운로드하는 불편한 과정을 거쳐야 합니다. <br> **✅ 해결책** 사용자 경험의 마찰을 줄이기 위해 개발자는 사용자에게 파일 관리를 위해 **별도의 앱을 설치해야 함을 명확히 안내**하거나, 프로젝트 성격에 따라 **앱 내부에 자체적인 사진 관리/미리보기 기능**을 구현하는 방안을 고려해야 합니다. <br><br><br> 이번 2부에서는 RayNeo X2 개발의 핵심인 1차원 터치 입력을 처리하는 방법과 카메라로 사진을 찍어 저장하는 실질적인 구현 과정을 살펴보았습니다. 특히 TTS 부재나 갤러리 앱 미지원과 같은 플랫폼의 독특한 한계점들을 사전에 인지하고 우회 전략을 세우는 것이 중요하다는 점을 강조했어요. RayNeo X2는 **개발자에게 가장 열려있는 AR 플랫폼**이지만, 이러한 하드웨어 및 소프트웨어의 미성숙함을 극복하는 것이 중요한 과제입니다. 이러한 경험들이 미래의 포용적 기술(Inclusive Technology) 개발에 소중한 밑거름이 될 것이라 확신합니다. <br><br><br>