
문제 상황 인식
React Native로 근무 일정 관리 앱을 개발하면서, 사용자 경험을 위해 앱 데이터를 기기에 안전하게 저장하는 게 필수였습니다.계정 로그인 없이도 데이터를 유지하려면, 로컬 저장소를 써야 하는데요.
초기에는 React Native 기본 제공인 AsyncStorage를 쓰려 했습니다. 그런데 데이터가 많아지면서 속도가 너무 느려졌고, 사용자 경험이 크게 떨어졌습니다.
SQLite도 고려했지만, 관계형 데이터베이스의 복잡한 설정과 오버헤드가 부담스러웠고, 저희 앱 구조에는 과하다고 판단했습니다.
결국 네이티브 C++로 구현되어 성능이 뛰어나고, Facebook에서 만든 MMKV를 선택했습니다.
MMKV가 뭐고 왜 좋은가?
MMKV는 메모리 매핑 방식을 활용하는 고성능 키-값 저장소입니다.
네이티브 코드로 구현되어 JS 스레드 부담 없음
대용량 데이터도 빠르게 읽고 쓸 수 있음
React Native에서 공식 지원되어 안정적임
덕분에 앱 실행 시 데이터 로딩 시간이 크게 단축됐고, 사용자 반응성이 좋아졌습니다.
핵심 문제: Zustand와 MMKV 통합
MMKV를 Zustand와 연결하려면 직접 어댑터를 만들어야 했습니다.
Zustand의 createJSONStorage
API를 활용해 MMKV를 감싸고,
저장할 때 Date와 Set 같은 JSON 변환 불가 타입을 문자열과 배열로 변환
불러올 때 원래 Date와 Set 객체로 복원
하는 로직을 구현했습니다.
import { MMKV } from "react-native-mmkv";
import { createJSONStorage } from "zustand/middleware";
const storage = new MMKV();
const mmkvStorage = createJSONStorage(() => ({
getItem: (key) => {
const json = storage.getString(key);
if (!json) return null;
const data = JSON.parse(json);
// Date, Set 복원 예시 (스케줄 데이터만)
if (key === STORE_NAMES.SCHEDULE && data.allSchedulesById) {
Object.values(data.allSchedulesById).forEach((schedule) => {
if (Array.isArray(schedule.selectedWeekDays)) {
schedule.selectedWeekDays = new Set(schedule.selectedWeekDays);
}
["startTime", "endTime", "startDate", "endDate"].forEach((field) => {
if (typeof schedule[field] === "string") {
schedule[field] = new Date(schedule[field]);
}
});
});
}
return data;
},
setItem: (key, value) => {
let serialized = value;
if (key === STORE_NAMES.SCHEDULE && typeof value === "object" && value !== null) {
serialized = { ...value };
if (serialized.allSchedulesById) {
Object.values(serialized.allSchedulesById).forEach((schedule) => {
if (schedule.selectedWeekDays instanceof Set) {
schedule.selectedWeekDays = Array.from(schedule.selectedWeekDays);
}
["startTime", "endTime", "startDate", "endDate"].forEach((field) => {
if (schedule[field] instanceof Date) {
schedule[field] = schedule[field].toISOString();
}
});
});
}
}
storage.set(key, JSON.stringify(serialized));
},
removeItem: (key) => {
storage.delete(key);
},
}));
스토어별로 persist 설정 분리
앱은 여러 역할별 스토어를 운영 중이라, 스토어마다 persist 설정을 분리해 유지보수성을 높였습니다.
const createPersistConfig = (name) => ({
name,
storage: mmkvStorage,
version: SCHEDULE_STORE_VERSION,
merge: (persistedState, currentState) =>
persistedState && Object.keys(persistedState).length > 0
? { ...currentState, ...persistedState }
: currentState,
});
이렇게 하면, 스토어에서 사용시 다음과 같이 간편하게 사용할 수 있습니다.
export const useDateScheduleStore = create<DateScheduleStore>()(
persist(
(set) => ({
dateSchedule: {},
addDateSchedule: (schedule: ScheduleByDate) =>
set((state: DateScheduleStore) => ({
dateSchedule: { ...state.dateSchedule, ...schedule },
})),
updateDateSchedule: (date: string, sessionIds: string[]) =>
set((state: DateScheduleStore) => ({
dateSchedule: { ...state.dateSchedule, [date]: sessionIds },
})),
removeDateSchedule: (date: string) =>
set((state: DateScheduleStore) => {
const { [date]: removed, ...remaining } = state.dateSchedule;
return { dateSchedule: remaining };
}),
clear: () => set({ dateSchedule: {} }), // 추가
}),
createPersistConfig(STORE_NAMES.DATE_SCHEDULE)
)
);
스케줄 데이터 관리 유틸리티 함수
이 컴포넌트에서 사용하는 useScheduleManager
훅 내부에서는 아래와 같은 유틸리티 함수들을 활용해 MMKV 저장소와 Zustand 상태를 함께 관리합니다.
export const scheduleStoreUtils = {
clearAllScheduleData: () => {
// MMKV 저장소 내 모든 관련 키 삭제
Object.values(STORE_NAMES).forEach((name) => {
storage.delete(name);
});
// Zustand 스토어 상태 초기화
const scheduleStore = useScheduleStore.getState();
const dateScheduleStore = useDateScheduleStore.getState();
const calendarDisplayStore = useCalendarDisplayStore.getState();
scheduleStore.clear();
dateScheduleStore.clear();
calendarDisplayStore.clearCalendarDisplay();
},
getStoredData: () => {
return Object.fromEntries(
Object.entries(STORE_NAMES).map(([key, value]) => [
key,
storage.getString(value),
])
);
},
};
clearAllScheduleData
: MMKV 저장소에서 관련 데이터 삭제 후 Zustand 상태 초기화까지 한꺼번에 수행합니다.getStoredData
: 현재 MMKV 저장소에 저장된 데이터를 그대로 확인할 수 있습니다.
이렇게 전체삭제나, 저장소 확인 동작을 수행하는 함수들을 모아서 재정의한 객체로 정의해서 사용성이 올라가도록 개발하였습니다.