공간 기억(Spatial Memory) 구축 파트 3: Go 백엔드를 활용한 모바일 AR 앱을 만들며 배운 점
요약
Go 백엔드와 ARCore를 활용하여 물리적 위치에 메모를 고정하는 AR 앱을 개발하며 겪은 기술적 도전과 해결 과정을 다룹니다. 특히 ARCore의 위치 추적 드리프트 문제를 GPS 재동기화를 통해 해결하는 실무적인 팁을 공유합니다.
핵심 포인트
- ARCore의 시각적 SLAM 기반 위치 추적은 이동 거리에 따라 드리프트 오차가 발생함
- GPS 데이터와 ARCore 위치 정보를 주기적으로 혼합하여 오차를 보정해야 함
- 공간 검색을 위해 PostGIS와 Redis GEO를 활용한 Go 백엔드 구축 사례
공간 기억(Spatial Memory) 구축 파트 3: Go 백엔드를 활용한 모바일 AR 앱을 만들며 배운 점
솔직히 말해서, 이 프로젝트에 관한 세 번째 글을 이렇게 빨리 쓰게 될 줄은 몰랐습니다. 공간 검색(spatial search)을 꽤 잘 작동하게 만든 후(혹시 지난 글을 놓치셨을 수도 있으니 말씀드리자면, 8초에서 20ms로 단축했습니다), 저는 "좋아, 이제 모바일 AR 프론트엔드만 얹으면 끝이네"라고 생각했습니다.
스포일러: 그렇게 간단하지 않았습니다.
실제 세상에서 제대로 작동하는 AR 앱을 만드는 것은 단순히 ARCore 프레임워크를 몇 개 집어넣고 끝내는 것보다 훨씬 더 많은 것이 필요하다는 것을 고생하며 배웠습니다. 3주 동안 이상한 문제들을 디버깅하고, 코드의 절반을 버리고, 동네를 빙빙 돌며 스마트폰을 쳐다본 끝에, 실제로 무엇이 효과가 있었고 무엇이 효과가 없었는지 공유하고자 합니다.
아이디어: "물리적 세계를 위한 Pinterest"
처음 오신 분들을 위해 상황을 설명해 드리자면: Spatial Memory는 메모, 사진, 추억을 물리적인 GPS 좌표에 "핀(pin)"으로 고정하는 앱입니다. 해당 위치에 실제로 물리적으로 가까이 있을 때만 핀을 볼 수 있습니다. 따라서 다음과 같은 일을 할 수 있습니다:
- 친구의 생일을 위해 아파트 문 앞에 친구를 위한 메모 남기기
- 등산로 입구에 하이킹 경로 가이드 핀 고정하기
- 사람들이 찾을 수 있도록 도시 곳곳에 "숨겨진 보물" 사진 찾기 게임 남기기
- 차를 어디에 주차했는지 기억하기 (물론 이건 이미 수만 번이나 구현된 기능이지만, 여전히 유용합니다)
저는 전체 백엔드를 공간 검색을 위한 PostGIS와 캐싱을 위한 Redis GEO를 사용하여 Go로 구축했습니다. 그것이 제가 첫 두 개의 글에서 다룬 내용입니다. 이제 모바일 프론트엔드를 만들 차례였습니다. 저는 네이티브 Android와 ARCore를 사용하기로 결정했습니다. 왜냐고요? ARCore가 평면 감지(plane detection)와 위치 추적(position tracking)을 위한 많은 힘든 작업을 대신 해주기 때문이죠, 그렇지 않나요?
첫 번째 놀라움: AR 위치 지정은 생각만큼 정확하지 않다
좋습니다, GPS가 완벽하지 않다는 것은 알고 있었습니다. 첫 번째 글에서도 그 점에 대해 썼었죠. 하지만 제가 예상하지 못했던 것은 ARCore의 위치 추적 드리프트(position tracking drift)가 시간이 지남에 따라 얼마나 심각해지는가였습니다.
현상은 이렇습니다: 앱을 시작하면 ARCore가 평면(plane)을 찾습니다. 그리고 휴대폰으로부터 초기 GPS 위치를 가져옵니다. 그 다음 당신은 걷기 시작합니다. ARCore는 시각적 SLAM (visual SLAM)을 수행하여 시작 지점 대비 당신이 어떻게 이동했는지를 추적합니다. 하지만 발걸음을 옮길 때마다 아주 미세한 오차가 추가됩니다. 50미터를 걸으면 2~3미터 정도 오차가 생길 수 있습니다. 100미터를 걸으면 그 드리프트(drift)는 5미터 이상이 될 수 있습니다.
앱 전체가 핀(pin)이 정확한 물리적 위치에 놓이는 것에 의존한다면 이것은 문제가 됩니다. 5미터의 드리프트는 당신이 실제로 핀을 놓은 위치가 아니라, 길 건너 공중에 핀이 떠 있는 상태가 될 수 있음을 의미합니다.
그래서 이를 해결하기 위해 무엇을 했을까요? 제가 결국 선택한 타협안들은 다음과 같습니다:
1. 정기적인 GPS 재동기화 (Regular GPS Re-syncing)
10초마다 현재 GPS 측정값을 가져와 ARCore의 위치와 혼합(blend)합니다. 완벽하지는 않지만, 드리프트(drift)를 일정 범위 내로 묶어둡니다. Kotlin에서 이 혼합이 대략적으로 어떻게 작동하는지는 다음과 같습니다:
fun updatePosition(arPosition: Vector3, gpsPosition: GpsCoordinate): GpsCoordinate {
// AR 상대 이동을 GPS 좌표로 변환
val arOffset = calculateGpsOffsetFromMovement(arPosition, initialGpsPosition)
...
단순하지만 효과적입니다. AR은 당신이 움직일 때 위치를 부드럽게 유지해주고, GPS는 실제 위치에서 너무 멀리 벗어나지 않도록 잡아줍니다.
2. 핀을 배치하기 전 "신선한 GPS" 요구
만약 지난 5초 동안 정확도가 10미터보다 나은 GPS 업데이트를 받지 못했다면, 사용자가 핀을 배치하지 못하게 막습니다. 대신 다음과 같은 작은 경고를 보여줍니다:
"더 나은 GPS 신호를 기다리는 중입니다... 하늘이 더 잘 보이는 곳으로 이동해 보세요."
번거로운 일이지만, 사람들이 의도한 위치와는 전혀 다른 곳에 핀을 배치하는 것을 방지해 줍니다. 잘못된 데이터가 들어가면(Bad data in) 잘못된 데이터가 나옵니다(bad data out). 길 건너편에 있어서 쓸모없게 된 핀을 갖느니, 몇 초를 기다리는 편이 낫습니다.
3. 완벽하지 않다는 점을 받아들이기
솔직히 말하자면? 어느 정도의 드리프트 (Drift)는 여전히 발생할 것입니다. GPS는 실내나 빌딩 숲(City canyons)에서 완벽하지 않습니다. AR 드리프트 (AR drift)는 실재하는 문제입니다. 저는 이것이 현재 하드웨어의 한계임을 받아들였습니다. 대신 FAQ에 다음과 같이 기록해 두었습니다: "핀의 위치가 어긋나 보인다면, 가까이 다가가세요. 그러면 앱이 결국 위치를 보정할 것입니다."
한계점에 대해 솔직하게 말하면 사용자들은 놀라울 정도로 이해해 줍니다.
두 번째 놀라움: 적절한 거리에서 핀을 렌더링하는 것은 까다롭다
ARCore는 포즈 행렬 (Pose matrix)이 포함된 카메라 뷰를 제공하며, 사용자는 원하는 위치에 앵커 (Anchor)를 배치합니다. 핀이 5~10미터 이내에 있을 때는 이 방식이 아주 잘 작동합니다. 하지만 위치 기반 앱의 특성상 100미터 이상 떨어진 핀이 있을 때는 상황이 이상해집니다.
핀이 카메라에서 멀어질수록 위치의 미세한 오차는 증폭됩니다. 100미터 거리에서 1미터의 위치 오차는 별것 아닌 것처럼 들릴 수 있지만, 실제로는 핀이 있어야 할 곳에서 훨씬 멀리 떠 있는 것처럼 보이게 만듭니다.
또한, AR에서 너무 많은 핀을 렌더링하면 프레임 레이트 (Frame rate)가 떨어집니다. 주변에 50개의 핀이 있는 공원에 있다면, 50개를 한꺼번에 렌더링하고 싶지는 않을 것입니다. 그것은 시각적 혼란을 야기할 뿐만 아니라 모든 동작을 느리게 만듭니다.
결국 제가 선택한 방법은 다음과 같습니다:
1. 두 가지 서로 다른 렌더링 모드
가까운 핀 (< 20미터): 물리적 세계에 고정된 완전한 3D 렌더링 (3D rendering). 사용자가 핀과 상호작용하고, 탭하고, 상세 정보를 볼 수 있습니다.
먼 거리의 핀 (20 - 200미터): 카메라 피드 위에 그려지는, 핀 방향을 가리키는 단순한 2D 빌보드 (Billboard). 3D 공간에 완벽하게 고정하려고 시도하는 대신, "저 방향에 핀이 있다"는 것을 알려줍니다. 단순히 방향만 알면 될 때는 완벽한 3D가 필요하지 않습니다.
다음은 거리 확인 로직의 단순화된 버전입니다:
fun shouldRenderAsBillboard(pin: Pin): Boolean {
val distance = calculateDistance(currentGps, pin.gpsCoordinate)
return distance > 20.0f // meters
...
단순하지만 효과적입니다. 이를 통해 프레임 레이트를 유지하고, 먼 거리에서 핀이 이상하게 떠다니는 증상을 방지할 수 있습니다.
2. 200미터 너머의 모든 것은 컬링 (Cull)
핀이 200미터보다 멀리 떨어져 있다면, 아예 보여주지 않습니다. 그게 무슨 의미가 있겠습니까? 어차피 상호작용할 수도 없는데 말이죠. 그냥 가까이 다가갔을 때만 보여주세요. 이렇게 하면 렌더링 부하 (rendering load)를 줄이고 장면을 깔끔하게 유지할 수 있습니다.
다시 말하지만, 이것은 타협안이며, 이런 종류의 앱에는 적절한 타협입니다.
3. 먼 거리의 핀을 위한 LOD 스케일링 (LOD Scaling)
멀리 있을 때는 핀을 더 작게 만듭니다. 당연한 소리처럼 들리시나요? 하지만 저는 이렇게 하지 않는 AR 앱을 정말 많이 보았습니다. 5미터 거리에서 2미터 크기인 핀은, 화면상에서 동일한 크기로 보이려면 50미터 거리에서는 10미터 크기가 되어야 합니다. 스케일링 (scaling)을 하지 않으면 먼 곳의 핀은 너무 작게 보여서 식별할 수 없게 됩니다.
fun getPinScale(distance: Float): Float {
val baseSize = 1.5f // meters
// 겉보기 크기가 일정하게 유지되도록 거리에 비례하여 스케일링
...
이것은 발견 가능성 (discoverability) 측면에서 게임 체인저 (game-changer)였습니다. 이 기능을 추가하기 전에는 사람들이 핀을 보지 못해 그냥 지나쳐 버리곤 했습니다. 하지만 이를 적용한 후에는 멀리 있어도 핀이 눈에 확 들어옵니다.
세 번째 놀라움: 배터리 소모는 실재한다
세상에. ARCore는 배터리를 잡아먹습니다. 정말 엄청나게 잡아먹습니다. 카메라를 연 상태로 AR 모드를 30분 동안 켜두면 휴대폰 배터리가 바닥날 것입니다. 아마 여러분은 "모든 AR 앱은 배터리를 소모해, 당연한 거 아냐?"라고 생각하실지도 모릅니다. 하지만 저는 얼마나 많이 소모할지는 예상하지 못했습니다.
제가 매일 사용하는 기기는 Pixel 7입니다. Spatial Memory를 AR 모드로 실행했을 때, 배터리 수명은 약 3시간 정도였습니다. 하루 종일 하이킹을 하며 사용할 수도 있는 앱치고는 도저히 받아들일 수 없는 수준이었습니다.
그래서 어떤 최적화 (optimizations)를 진행했을까요? 가장 큰 차이를 만든 것들은 다음과 같습니다.
1. 앱이 백그라운드에 있을 때의 "저전력 모드"
Android는 앱이 백그라운드로 들어갈 때 콜백 (callback)을 제공합니다. 앱이 백그라운드로 전환되면 저는 즉시 ARCore 세션 (session)을 일시 중지하고 모든 렌더링을 중단합니다. 그냥 백그라운드에서 계속 돌아가게 두지 마세요. 사용자는 앱을 닫는 것을 잊어버릴 수 있고, 그렇게 되면 결국 배터리가 방전됩니다.
override fun onPause() {
super.onPause()
if (arSession != null) {
...
당연해 보일 수도 있지만, 저는 처음에 이 과정을 빠뜨렸습니다. 큰 실수였죠. 여러분은 저와 같은 실수를 하지 마세요.
2. GPS 모드 전용으로 AR을 비활성화하는 옵션
많은 경우, 사람들은 단순히 핀(pin)을 찾고 싶어 할 뿐 증강 현실 (AR)이 필요하지는 않습니다. 그래서 저는 "AR 모드 On / Off" 토글을 추가했습니다. AR이 꺼져 있으면 지도와 핀을 가리키는 나침반만 나타납니다. 카메라도, ARCore도 사용하지 않으므로 배터리가 5배 더 오래 지속됩니다.
이 기능은 제가 단순히 핀을 찾기 위해 하이킹을 할 때 즐겨 사용하는 방식입니다. AR 효과가 필요하지 않은데 왜 배터리를 소모해야 할까요?
3. 움직임이 없을 때 위치 업데이트 제한 (Throttle)
저는 사용자가 정지 상태인지 감지하기 위해 Android Activity Recognition API를 사용합니다. 사용자가 30초 동안 움직이지 않으면, GPS 업데이트 속도를 1Hz에서 0.1Hz(10초마다 한 번)로 낮춥니다. 사용성에는 영향을 주지 않으면서도 놀라울 정도로 많은 배터리를 절약할 수 있습니다.
다시 한번 말씀드리지만, 단순한 최적화가 큰 효과를 가져옵니다.
코드 스니펫: 나의 단순화된 AR 위치 융합 (Position Fusion)
AR 트래킹과 GPS를 융합하기 위해 제가 사용하는 실제 (단순화된) 코드를 보여드리겠습니다. 엄청난 기술은 아니지만, 블렌딩 (blending)을 제대로 맞추는 데 꽤 오랜 시간이 걸렸습니다.
먼저, 시작 지점을 추적합니다:
data class GpsCoordinate(val latitude: Double, val longitude: Double)
class ArGpsFuser {
...
이것이 제 솔루션의 핵심입니다. 문제가 발생했을 때 디버깅할 수 있을 만큼 충분히 단순하며, 이는 화려한 기술을 사용하는 것보다 훨씬 더 중요합니다.
장단점: 3주 후 사용 결과는 어떤가?
솔직히 말씀드리겠습니다. 이것은 저의 사이드 프로젝트이며, 완성된 제품이 아닙니다. 있는 그대로 분석해 보겠습니다:
장점 ✅
-
실제로 작동한다 (It actually works). 좋은 GPS 수신 환경에서 외부에 있을 때, 핀(pin)은 1~2미터 이내에 배치한 곳에 그대로 유지됩니다. 사용 사례에는 충분히 좋습니다.
-
**2단계 렌더링(two-tier rendering: 근거리 풀 3D, 원거리 빌보드)**이 정말 잘 작동합니다. 더 이상 떠다니는 핀의 어색함은 없고, 주변에 20개의 핀이 있어도 프레임 속도가 60fps 이상을 유지합니다.
-
최적화 덕분에 배터리 수명이 양호합니다 (Battery life is acceptable). AR 모드를 켜고 끄는 것을 반복해도 약 6~7시간 정도 사용 가능하며, 이는 하루 동안 하이킹하기에 충분한 시간입니다. 저전력 모드는 훨씬 더 오래갑니다.
-
Go 백엔드가 사이드 프로젝트로 매우 잘 확장됩니다 (Go backend scales really well). 저는 Fly.io의 무료 티어(free tier)를 사용하여 호스팅하고 있는데, 제가 던지는 모든 것을 처리합니다. 콜드 스타트(Cold starts)는 짜증나지만, 사용자 몇 명만 있는 사이드 프로젝트로는 괜찮습니다.
단점 ❌
-
실내 AR은 여전히 상당히 안 좋습니다 (Indoor AR is still pretty bad). GPS가 실내에서는 작동하지 않으며, 시각적 표류(visual drift)가 심합니다. 현재는 실내 사용을 광고하지 않습니다. 아마도 ARCore의 글로벌 위치 파악 기능이 개선될 때쯤일 것입니다.
-
기기마다 AR 성능 편차가 매우 큽니다 (Different phones have wildly different AR performance). 제 Pixel 7은 아주 잘 작동합니다. 오래된 Samsung A50은 30fps를 유지하는 데 어려움을 겪습니다. 안드로이드의 본질적인 문제이지만, 여전히 답답합니다.
-
콜드 스타트가 문제입니다 (Cold start is a problem). 모든 위치 기반 소셜 앱과 마찬가지로, 유용한 기능을 하려면 해당 지역에 충분한 핀이 있어야 합니다. 현재는 제가 유일하게 테스트하고 있기 때문에 모든 핀이 제 동네에 있습니다. 이는 오직 성장이 해결할 수 있는 '닭이 먼저냐 달걀이 먼저냐' 문제일 뿐입니다.
-
Apple의 지원이 아직 없습니다 (Apple support doesn't exist yet). 저는 안드로이드 사용자이고 아주 오래된 Mac을 하나 가지고 있기 때문에 Android만을 위해 만들었습니다. 만약 iOS/ARKit 관련 도움을 주고 싶은 분이 있다면 알려주세요!
다시 할 것인가?
네, 솔직히 말해서요. 이것을 구축하면서 정말 많은 것을 배웠습니다. 처음 시작했을 때 저는 그저
제가 계속해서 반복하며 배우고 있는 가장 큰 교훈은 이것입니다: 현대적인 하드웨어로 어려운 문제를 해결하더라도 여전히 많은 작은 타협 (compromises)이 필요하다는 점입니다. 모든 튜토리얼은 기본적인 기능을 작동시키는 방법은 보여주지만, 그것이 제대로 작동하게 만들기 위해 필요한 모든 미세한 조정들에 대해서는 말해주지 않습니다.
AR 위치 측정 (AR positioning)은 완벽하지 않기에 GPS와 혼합하여 사용합니다. 멀리 있는 핀(pin)에는 전체 3D 렌더링 (3D rendering)이 작동하지 않기에 2D 빌보드 (2D billboards)를 사용합니다. AR은 배터리를 소모하기에 저전력 모드를 추가합니다. 이 모든 것이 트레이드오프 (tradeoffs)입니다.
만약 여러분이 직접 AR 위치 기반 앱을 구축할 생각을 하고 있다면, 제 조언은 이렇습니다: 단순하게 시작하세요. 처음부터 너무 거창한 목표를 세우지 마세요 (Don't try to boil the ocean). 한 가지 기능을 제대로 작동하게 만든 다음, 그다음 기능을 추가하세요. 그리고 거실에서만 테스트하지 말고 실제 외부 환경에서 테스트하세요. 동네 한 바퀴를 걷기 시작했을 때야 비로소 나타난 문제들이 얼마나 많았는지 다 말할 수 없을 정도입니다.
프로젝트를 확인하거나 기여하고 싶다면 GitHub에서 완전히 오픈 소스로 공개되어 있습니다:
👉 https://github.com/kevinten10/spatial-memory
백엔드는 모두 그곳에 있으며, 안드로이드 앱도 진행 중입니다. 기여할 사이드 프로젝트를 찾고 계신다면 아직 할 일(TODOs)이 많이 남아 있습니다.
여러분을 위한 질문
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기