Jotai + MMKV로 React Native 상태 관리 및 영속화 구현기 - 한의원 모바일 앱 프로젝트 적용 사례
1. 배경 및 기술 선택 이유
React Native(Expo) 기반으로 개발하면서, 인증 상태를 포함한 전역 상태를 안정적으로 관리하고 앱 재시작 시에도 유지해야 하는 요구사항이 있었다.
초기에는 React Context + AsyncStorage 조합을 검토했으나, 두 가지 문제가 있었다. 첫째, Context 기반의 전역 상태는 Provider 중첩이 깊어지면서 관리의 복잡도가 급격히 증가했다. 둘째, AsyncStorage는 비동기 API만 제공하기 때문에 앱 초기화 시 인증 토큰을 읽어오는 과정에서 불필요한 로딩 화면이 발생했다. 이를 해결하기 위해 Jotai(상태 관리)와 MMKV(동기식 영속 스토리지) 조합을 선택했다.
2. 핵심 기술 개요
2.1 Jotai - 원자적 상태 관리
Jotai는 atom 단위로 상태를 정의하는 경량 상태 관리 라이브러리이다. Redux와 달리 보일러플레이트가 거의 없고, React의 useState와 유사한 인터페이스(useAtom)를 제공하기 때문에 학습 곡선이 낮다. 각 atom은 독립적으로 구독되므로 불필요한 리렌더링을 최소화할 수 있다.
2.2 MMKV - 동기식 네이티브 스토리지
MMKV는 WeChat에서 개발한 고성능 키-값 저장소로, React-native-mmkv 라이브러리를 통해 React Native에서 사용할 수 있다. AsyncStorage와의 결정적 차이점은 동기식(synchronous) 읽기/쓰기를 지원한다는 점이다. 이 덕분에 앱 시작 시 저장된 토큰을 즉시 읽어 초기 상태에 반영할 수 있다.
| 비교 항목 | AsyncStorage | MMKV |
|---|---|---|
| 읽기/쓰기 | 비동기 (await 필요) | 동기식 (즉시 반환) |
| 성능 | 상대적으로 느림 | 약 30배 빠름 |
| 초기화 시 상태 복원 | 로딩 화면 필요 | 즉시 복원 가능 |
| 암호화 지원 | 별도 라이브러리 필요 | 내장 지원 |
3. 적용 과정 및 구현
3.1 스토리지 추상화 레이어 설계
Welltizen은 모바일 앱과 관리자용 웹 인터페이스가 공존하는 구조였기 때문에, 플랫폼에 따라 스토리지 구현체를 교체할 수 있는 추상화 레이어를 먼저 설계했다. 인터페이스를 정의하고 모바일에서는 MMKV를, 웹에서는 localStorage를 주입하는 방식으로 구현했다. 이를 통해 비즈니스 로직은 스토리지 구현체에 의존하지 않게 되었다.
3.2 Jotai atomWithStorage 패턴 적용
Jotai의 atomWithStorage 유틸리티를 활용해 atom의 값이 변경될 때 자동으로 MMKV에 영속화되도록 구성했다. 핵심은 커스텀 storage 어댑터를 작성하여 Jotai가 기대하는 getItem/setItem/removeItem 인터페이스에 MMKV의 동기 API를 연결한 것이다. 이를 통해 인증 토큰, 사용자 프로필 등을 atom 단위로 관리하면서 앱 종료 후에도 상태가 자연스럽게 복원되도록 했다.
3.3 Axios Interceptor와의 통합
인증 토큰이 Jotai atom에 저장되어 있으므로, Axios 요청 인터셉터에서 이 값을 읽어 Authorization 헤더에 자동 주입하는 구조를 적용했다. 다만 웹에서는 인터셉터가 모듈 스코프에서 한 번 초기화되는 반면, React Native에서는 컴포넌트 라이프사이클과 맞물려 초기화 시점이 달라지는 문제가 있었다. 이를 MMKV의 동기 읽기를 활용해 인터셉터 내에서 직접 토큰을 읽어오는 방식으로 해결했다.
4. 주요 문제 및 해결 과정
4.1 Jotai Atom 초기화 타이밍 문제
가장 까다로웠던 문제는 Jotai atom의 초기화 타이밍이었다. atomWithStorage로 정의한 atom이 Provider 마운트 전에 기본값으로 초기화되면서, MMKV에 저장된 실제 값이 무시되는 현상이 발생했다. 구체적으로는 앱을 재시작하면 로그인 상태가 풀리는 문제였다.
원인을 분석한 결과, Jotai의 Provider가 atom 값을 hydrate(초기화)하기 전에 하위 컴포넌트가 렌더링되면서 기본값(null)을 읽는 것이 문제였다. 해결 방법으로 useHydrateAtoms 훅을 활용하여 Provider 마운트 시점에 MMKV에서 읽은 값으로 atom을 즉시 hydrate(초기화)하는 초기화 컴포넌트를 만들었다. 이를 통해 앱 시작 시 로딩 없이 기존 인증 상태가 즉시 복원되었다.
4.2 웹-앱 간 Atom 캐싱 불일치
관리자 웹(React/TypeScript)과 모바일 앱이 동일한 Jotai atom 정의를 공유하는 구조에서, 웹의 localStorage와 모바일의 MMKV 간 직렬화 포맷 차이로 인해 데이터 파싱 오류가 발생했다. 이를 스토리지 어댑터 레이어에서 JSON.stringify/parse를 통일적으로 처리하도록 수정하여 해결했다.
5. 적용 결과 및 배운 점
Jotai + MMKV 조합을 통해 달성한 주요 성과는 다음과 같다.
- 앱 시작 시 인증 상태 즉시 복원 — 별도 로딩 화면 제거로 사용자 경험 개선
- 상태 관리 코드 간소화 — Redux 대비 약 60% 이상 보일러플레이트 감소
- 크로스플랫폼 호환성 확보 — 추상화 레이어를 통해 웹/앱 동일 로직 사용
- 스토리지 성능 향상 — AsyncStorage 대비 읽기/쓰기 속도 체감 수준으로 개선
이번 적용을 통해 배운 가장 중요한 교훈은, 크로스플랫폼 개발에서 "웹에서 잘 되던 패턴이 모바일에서도 동일하게 동작할 것"이라는 가정이 위험하다는 점이다. 특히 상태의 초기화 시점, 스토리지 접근 방식(동기/비동기), 컴포넌트 라이프사이클의 차이를 사전에 충분히 이해하고 설계에 반영해야 한다. Jotai와 MMKV는 이러한 차이를 효과적으로 메울 수 있는 실용적인 조합이며, 유사한 요구사항을 가진 React Native 프로젝트에 적극 추천한다.
IAN