아름다운 3D 오브젝트와 애니메이션은 언제 사용할까? 단순히 화려한 화면을 만드는 것뿐만 아니라, 사용자에게 어색하고 어렵게 느껴질 수 있는 과학 지식을 전달해야 할 때에도 매우 효과적이다.
오로라 앱에서 사용자 질문은 명확하다. 지금 볼 수 있는지, 상태가 좋아지는지, 이동이 필요한지다. 그래서 화면도 같은 구조로 나눴다. 전세계적인 오로라 현황을 3D 가시화로, 사용자 현위치의 상황을 애니메이션으로, 복잡한 데이터는 차트로 표현했다. 모바일 환경에서 이를 구현하고 안정적인 운영을 위해 성능을 최적화한 과정을 소개한다.
1. Three.js를 사용한 3D 지구
Aurora Oval 은 오로라 관측 확률을 보여주는 지도다. 대부분의 오로라 앱은 2D 지도를 사용하지만, 나는 3D 지구 위에 이 확률을 그려 더 직관적으로 표현하고자 했다. Expo에서 3D 오브젝트를 렌더링하고 만들기 위해 아래 라이브러리들을 활용했다.
@react-three/fiber: React 컴포넌트 방식으로 3D 씬 구성three: 카메라, Material, Shader 등 렌더링 코어@react-three/drei: GLTF 로딩 등 보조 유틸expo-gl: Expo 환경에서 WebGL 컨텍스트 연결r3f-native-orbitcontrols: 터치 회전/줌 인터랙션 제어
처음에는 정확도와 아름다움을 위해 오발을 최대한 원본 데이터 그대로 표현하려고 했으나, 프로덕션 안드로이드 환경에서 버벅임과 성능 문제로 앱이 종료되는 문제가 발생했다. 원인과 해결 방법을 1:1로 정리하면 아래와 같다.
-
원본 데이터는 위경도 1도 간격 격자로 65,160개의 포인트를 그대로 렌더링 시,
메모리와 렌더링 부담이 급격히 커졌다.
- 해결 방법: 3D 렌더 전에 강도값을 정규화하고 확률 20% 미만 데이터를 제거한 뒤, 격자 기반 압축을 적용해 포인트 수를 줄였다.
-
그럼에도 화면에 동시에 그려야 하는 포인트가 많고 포인트 간 밝기 합성 연산이
겹치면서, 한 프레임에서 처리해야 할 작업량이 60fps 기준 예산(약 16ms)을 자주
넘겨 프레임 드랍이 발생했다.
- 해결 방법: 레이어당 포인트 수, 전체 포인트 수, 동시 에너지 레벨 수를 제한해 한 번에 그리는 양의 상한을 고정했다.
-
Android WebGL 환경은 기기별 컨텍스트/드라이버 편차가 커서 순간 부하 시
프레임 드랍이나 컨텍스트 오류가 발생했다.
- 해결 방법: 오류 대응을 "실패를 전제로" 설계해, 오로라 레이어를 잠시 비활성화하고 자동 재시도하는 복구 경로를 넣어 화면 전체가 죽지 않게 했다.
이번에 얻은 깨달음은, 3D 렌더링 품질을 올리는 것도 중요하지만 운영 환경에서 안정적으로 동작하도록 성능 개선을 함께 설계해야 한다는 점이었다.
2. Reanimated, Svg를 사용한 2D 애니메이션
오로라 예보에 쓰이는 지표(태양풍, 자기장, 구름량)를 함께 보면 내 위치의 오로라 확률을 더 정확하게 가늠할 수 있다. 다만 초보 오로라 헌터에게는 이런 수치를 실시간으로 해석하는 일이 어렵다. 그래서 지표를 숫자로 나열하는 대신, 2D 일러스트 기반의 실시간 예보 화면으로 직관적으로 확인할 수 있게 구성했다.
-
react-native-reanimated: 카드/배경/전환 모션을 UI 스레드에서 처리 react-native-svg: 벡터 일러스트와 입자 레이어 렌더링
react-native-reanimated의 핵심 장점은 애니메이션 계산을 JS
thread가 아니라 UI thread(worklet)에서 수행한다는 점이다. 그래서 네트워크
응답, 상태 계산, 로깅 등으로 JS thread가 순간적으로 바빠져도 카드 전환/배경
모션/별 반짝임 같은 프레임 루프는 상대적으로 안정적으로 유지할 수 있었다.
react-native-svg의 장점은 "표현력과 확장성"이었다. 오로라 밴드,
태양풍 입자, 자기장 곡선 같은 요소를 Path, Line,
Circle, LinearGradient 조합으로 정밀하게 만들 수
있었고, 같은 도형 구조를 재사용해 다양한 상황(강도/속도 변화)에도 일관된 2D
예보 화면을 유지할 수 있었다.
3. react-native-gifted-charts를 사용한 태양풍 차트
-
react-native-gifted-charts: 태양풍 시계열(속도/밀도/Bt/Bz) 라인 차트 렌더링과 포인터 상호작용 처리 -
LazyMount + Suspense: 무거운 차트/일러스트 영역을 지연 마운트하고 스켈레톤 폴백으로 초기 진입 비용 분산
react-native-gifted-charts는 라인, 축, 그리드, 포인터 같은 필수
기능을 빠르게 갖출 수 있어 선택했다. 덕분에 차트 엔진 구현보다 태양풍 해석
UX에 집중할 수 있었다. 또한 포인터 이벤트와 라인 스타일, 툴팁/오버레이를 화면
목적에 맞게 확장하기 쉬워 운영 단계에서도 유연하게 대응할 수 있었다.
화면 구성에서는 LazyMount + Suspense를 적극적으로 사용했다. 화면
진입 시 모든 블록을 한 번에 렌더링하지 않고, 데이터 준비 여부와 가시성에 맞춰
섹션 단위로 늦게 올렸다. 이 방식은 태양풍 차트뿐 아니라 애니메이션, 일러스트,
기타 무거운 화면에서도 공통 전략으로 적용해 초기 렌더 부담을 분산하는 데 가장
효과적이었다.
마무리하며
이번 작업을 통해 가장 크게 느낀 점은, 크로스 플랫폼 앱은 다양한 환경(OS, 기기)에 적응 가능하도록 성능을 신경써야 한다는 점이었다. 모든 문제를 한 번에 해결하기는 어렵지만, 서비스 안정성과 사용자 경험 사이에서 균형을 잘 잡아야 한다.


