기본 환경
React Native 기반 서비스를 구현하기 위해 공식 문서를 참고하여 환경을 구성했다. 개인적으로 Expo 사용을 지양하는 편이라 기본 React Native 기준으로 프로젝트를 구성했다.
New Architecture
처음으로 진행했던 작업은 React Native 네이티브 애플리케이션의 New Architecture(Fabric)를 활성화 시키는 것이었다. React Native 팀에서 상당한 공을 들여 새로운 아키텍처 기반으로 동작하는 React Native 버전을 공개했고, 앞으로 React Native 는 해당 아키텍처 기반으로 동작하도록 방향이 바뀔 조짐이 보이기에 한 번 사용해보고자 했다.
회사 팀 내에서 관련 내용에 대해 간략히 이야기 했기도 했고, 관심이 있었기에 새로운 아키텍쳐 기반으로 서비스를 구현하려고 했고, 빌드에도 큰 문제 없이 Android, iOS 모두 정상적으로 진행되었다.
지금은 어느정도 개발이 완료되어 슬픈 이야기를 하려고 하는데, 아직 New Architecture 는 시기상조라고 생각한다.
기본적으론 빌드에도 이슈가 없고 잘 동작하지만, 일부 라이브러리들이 이를 지원하지 않거나 아직 실험 단계이기에 기존에 구현되어있던 애플리케이션에는 섣불리 전환하지 말라고 이야기 하고 싶다. 물론 사용하고 애플리케이션에서 사용하고 있는 SDK, 프레임워크, 라이브러리에 따라 다를 수 있다.
본인은 초기 프로젝트에서부터 시작했음에도 겪었던 이슈는 아래와 같다.
- Detox 미지원
- 2022.12.05 에 직접 이슈 코멘트 남겼으나 여전히 미해결 (2023.05.31 기준)
https://github.com/wix/Detox/issues/3720#issuecomment-1336499317
- 2022.12.05 에 직접 이슈 코멘트 남겼으나 여전히 미해결 (2023.05.31 기준)
- React Native 에서 제공하는 Modal 의 레이아웃 깨짐 이슈
- Re-render 시 레이아웃 정상으로 돌아오는 이슈
- react-native-reanimated v3 에서 레이아웃 애니메이션 미동작 (iOS)
- 2022.08.14 에 이슈업, 그러나 미해결 (2023.05.31 기준)
- https://github.com/software-mansion/react-native-reanimated/issues/3439
- 그 외 여러가지 자잘한 이슈들
주요 라이브러리들은 대부분 완벽히 동작한다. 하지만 위에 이야기 했던 것처럼 자잘한 이슈가 있기에 완벽히 사용하긴 어려웠다. 덕분에 엄청난 삽질을 하기도 했고, 실제로 사용 불가한 수준인 경우도 있었기에 아직 New Archtecture 베이스로 애플리케이션을 개발하기엔 어려움이 있다고 판단하여 우선 기존 아키텍쳐 기반으로 개발을 진행하기로 했다.
UI
화면을 구성하는데 있어 네이티브(Android, iOS) 그리고 웹(Web) 모두 동일한 코드 베이스를 사용하는 것을 목표로 했기에 고민을 많이 했었다. 최근에는 좋은 선택지가 많아 Tailwind CSS, Stitches 등 여러가지 선택지가 있는데 각 플랫폼에 맞는 라이브러리를 사용해야 한다는 단점이 있었다. 그러던 중 이전부터 눈여겨본 Dripsy 를 보았는데 네이티브 그리고 웹 모두 완벽히 지원하고 프로젝트의 목적에 아주 잘 맞기에 이를 기반으로 뷰를 구성하기로 했다.
모든 플랫폼을 커버하는 것도 큰 장점이었지만, 테마, 반응형 그리고 완벽한 타입 추론 지원 등 여러모로 적합했기에 선정하게 되었다. 덕분에 특정 플랫폼 기능을 사용하는 것이 아닌 이상 모든 UI 구성 요소는 한 코드로 구현함과 동시에 세 가지 플랫폼을 커버할 수 있게 되었고, 개발 경험도 상당히 좋았다.
아래 사진은 AppBar 컴포넌트 예시인데, 하나의 코드로 모든 플랫폼 모두 동일한 모습의 UI 를 구현한 결과물이다.
기본적으로 반응형 디자인을 고려하여 개발했기에 휴대폰, 태블릿, PC 모두 완벽하게 동작한다. 물론 화면을 회전시켜도 의도한 레이아웃이 잘 유지되고 있다.
Animations
애니메이션을 구현하기 위해 React Native 에서 제공하는 Animated API 그리고 Reanimated v3, Moti 를 사용했다. 네이티브와 웹 모두 완벽하게 동작하기에 큰 어려움은 없었지만, 아래와 같은 이슈가 있기도 했다.
- React Native 에서 고주사율 애니메이션을 지원하지 않음 (관련 이슈)
- Fabric 활성화 시 Reanimated 의 Layout Animation 동작하지 않음 (관련 이슈)
앞서 이야기 했듯이 Fabric 을 포기하게된 이유도 위와 이슈 때문이다. 요즘 출시되는 모바일 기기들은 대부분 고주사율 디스플레이를 탑재하고 있는데 이번에 React Native 가 60fps 애니메이션까지 지원한다는 점을 알게 되어 다소 아쉬움이 많이 남았다. 2020년도에 이슈업 되었는데 아직도 지원하지 않고 있다. (실제 React Native 코드를 열어보면 애니메이션이 60fps 로 동작하도록 하드픽스 되어있는 부분들이 많이 있다)
그러나 Reanimated 로 구현한 애니메이션은 디바이스 주사율에 맞게 잘 동작하기에 애플의 ProMotion 기술이 적용된 기기 등에서는 부드러운 120hz 애니메이션을 제공할 수 있었다. 약간의 희망도 잠시, 아쉬운 점이 하나 더 있는데 네비게이팅 처리를 위해 사용하고 있는 @react-navigation/stack 에서는 Reanimated 가 아닌 React Native 의 Animated API 를 기반으로 애니메이션을 처리하고 있어 화면 전환 애니메이션이 60fps 로 제한된다는 점이다.
마지막으로 Moti 의 경우 위 Dripsy 의 개발자가 만들어 배포한 라이브러리이다. 네이티브 및 웹 애니메이션을 위해 구현된 라이브러리이며 Reanimated 를 기반으로 동작하고 아주 쉽게 애니메이션을 구현할 수 있다. 웹에서 CSS 로 애니메이션을 구현했던 것보다 훨씬 쉽기도 하면서 아무 매끄럽게 동작한다. 이런 라이브러리를 만들어준 개발자에게 감사하다는 이야기를 전하고 싶다.
State Management
앱 전반적인 상태 관리를 위해 고민을 많이 했었다. 기본적으로 앱 전역 상태와 컴포넌트간의 공유 상태를 나누었는데 이를 처리하기 위해 아래 라이브러리를 사용하게 되었다.
앱 전역 상태를 위해 XState 를 선정한 이유는, Up 의 전역 상태의 경우 사용자 정보가 존재하는지(인증), 없는지에 따라 화면 이동이 달라진다는 점이었다. 간단히 생각해보면 인증 상태가 아닌 경우 로그인 그리고 회원가입을 할 수 있고, 인증 상태라면 기존 로그인한 사용자의 인증 정보 검증 그리고 로그아웃과 같이 상태에 따라 처리되는 기능이 명확하게 정해져 있다는 것을 알 수 있다. 이때 유한상태기계를 활용하면 이를 그대로 정의할 수 있게 되는데, 이 구현체가 XState 이기에 이를 활용하게 되었다.
그 외로 컴포넌트간 상태 공유를 위해 Recoil 을 사용했다. Jotai, Zustand 등 여러가지 상태 관리 라이브러리가 있지만, Recoil 을 한 번 사용해보고자 선정하게 되었다. 물론 복잡한 상태 공유가 요구되었던 상황은 아니었기에 어떤 라이브러리를 사용하더라도 문제는 없었을 것이다.
Data Storage
Up 의 경우 사용자 프로필 정보 뿐만 아니라 임무 그리고 달성 기록 등의 데이터를 저장해야 했다.
프로필 정보는 이름, 레벨 등 간단한 정보로 이루어져있기에 AsyncStorage 를 통해 JSON 데이터를 stringify 하여 저장하도록 구현했다. AsyncStorage 의 경우 네이티브(Android, iOS) 뿐만 아니라 웹(Web)도 지원하기에 이용하게 되었다.
그외 임무 그리고 달성 기록의 경우 여러개의 레코드로 이루어지는 데이터이기에 JSON 으로 처리하기엔 고려할 문제가 많았다. 이를 해결하기 위해 네이티브(Android, iOS)에서는 SQLite 기반의 데이터베이스를, 웹(Web)에서는 IndexedDB 를 활용하여 데이터를 처리하고자 했다.
네이티브 SQLite 를 활용하기 위해 react-native-sqlite-storage 라이브러리를 사용했고, 직접 테이블(Table)을 만들어 Up 내 모듈에서 SQL 구문을 통해 데이터 제어를 진행하고 있다.
웹의 IndexedDB 는 bxd 를 사용하고 있는데, 이는 필자 본인이 과거에 개발했던 IndexedDB 를 위한 ORM 이다.
두 데이터 저장소는 서로 다르기에 호환되지 않지만 하나의 인터페이스를 정의하여 두 저장소 모두 같은 동작을 수행할 수 있도록 구현하여 사용하고 있다.
React Native Web
웹 환경에서도 서비스를 지원하기 위해 react-native-web 을 사용했다. React Native 기본 프로젝트를 구성하면 Metro 구성만 존재하기에 별도로 Webpack 구성을 추가했다. 사용하고 있는 일부 라이브러리로 인해 빌드 환경을 구성하는데 어려움이 있었지만 번들링 시 Babel 을 통해 특정 패키지 코드를 별도로 transform 할 수 있도록 구성을 추가하여 해결했다.
React Native 도 이제 React 18 을 지원하기에 웹도 React 18 을 사용하고 있다. React Native Web 버전도 함께 업데이트를 해주어야 React 18 을 지원하는 React dom 이 사용되는데, 최신 버전의 React Native Web 을 사용하는 경우 Reanimated 에서 에러가 발생하고 있어 아직 미흡한 부분이 남아있다. (관련 이슈)
Rspack
초기에는 Webpack 기반으로 웹을 빌드하도록 했으나, GitHub 구경을 하는 중 Rspack 을 발견했다. Rust 가 요즘 핫하기도 했고, 이때 아니면 언제 사용보나 싶어서 Rust 기반의 번들러를 사용해보고자 구성을 변경하게 되었다. 이를 사용하고자 한 가장 큰 이유는 기존 Webpack 과의 호환성이 어느정도 보장이 되었기 때문에 쉽게 마이그레이션 할 수 있었고, 언어 특성에 따른 빌드 퍼포먼스도 매력적이었기에 선택했다.
실제로 큰 문제 없이 마이그레이션을 진행했고, 덕분에 빌드 속도를 획기적으로 줄일 수 있기도 했다.
(기존 약 50초 > 개선 후 약 1.5초로 개선)
Storybook
네이티브(Android, iOS)와 웹(Web)간의 UI 를 확인하며 개발하기 위해 Storybook 환경을 구성하기로 마음먹었다. 작업 당시에 React Native Storybook 의 버전은 6.x beta 버전이었는데, 지금은 6.5.x stable 버전이 릴리즈 되었다.
기존에 존재하던 5.x 버전과 달리 변경사항이 많았기에 GitHub 저장소를 둘러보며 이슈를 해결하고자 삽질했던 기억이 난다.
네이티브 외에도 웹 환경의 구성 이슈도 있었는데, 앞서 이야기 했던 것처럼 문제가 발생하던 특정 모듈을 transform 구성에 추가하는 방향으로 해결할 수 있었다. (지금은 별거 아닌 것처럼 이야기 하고 있지만 당시에는 몇 일 동안 붙잡고 삽질했었다)
참고로 Up 웹 애플리케이션은 Rspack 번들러 기반으로 빌드되고, Storybook 웹 애플리케이션은 Webpack 기반으로 빌드되고 있다. Up 프로젝트 내에 Metro, Rspack, Webpack 세 가지 번들러가 존재하는 셈. 덕분에 초기 빌드 환경을 바로잡기 위해 꽤나 골치가 아팠다.
PWA
웹 환경에서도 최대한 사용성을 개선하고자 PWA를 지원하도록 구현했다. 주 목적은 오프라인 지원과 캐싱이다.
페이지에 처음 접속할 때 페이지, 스크립트, 이미지를 Preload 하는데 서비스 워커를 통해 페이지 구성에 필요한 리소스는 모두 브라우저의 캐시 스토리지에 캐싱된다.
이후 페이지에 다시 접속하게 되면 기존에 캐싱했던 리소스가 있기에 상당히 빠른 속도로 페이지를 제공할 수 있고, 설령 사용자의 디바이스의 네트워크 상태가 좋제 않거나, 오프라인이더라도 페이지 로드는 문제 없이 이루어지기 때문에 일반 웹페이지보다 좋은 경험을 줄 수 있다고 기대한다.
Sentry
배포 이후 장애 대응을 위해 Sentry 연동도 진행했다. 물론 Native(Android, iOS) 그리고 웹(Web) 모두.
실제로 테스트 했을 때 정상적으로 에러가 수집되었고, 앞으로 일어날 일들에 대비하여 꾸준한 모니터링을 진행할 예정이다.
개발을 진행하며 부딛혔던 내용이 상당히 많지만 막상 글로 작성하려고 하니 어떤 내용을 적어야 할지 정리가 잘 안되는 것 같기도 하다. 추가로 궁금한 점은 댓글 혹은 메일로 보내주면 답변하도록 하고, 다음 글에서는 개발 후 실제 스토어에 배포하는 과정과 아쉬웠던 이야기를 하며 글을 마무리 하려고 한다.