본문 바로가기

Capacitor의 Key-Value 저장소: Preferences vs SQLite

Capacitor의 key-value storage 비교: Preferences, SQLite key-value store, Keychain storage. 설정, 캐시, 시크릿에 어떤 저장 방식이 적합한지 알아보세요.

Capacitor의 key-value storage 옵션 비교를 위한 Capgo 로고

Capacitor에서 key-value storage를 사용하려면 @capacitor/preferences 소수의 작은 설정에는 간단한 저장소를, 더 크거나 더 많은 항목에는 SQLite 기반 key-value store를, secrets에는 Keychain 또는 Keystore를 사용하세요. 이 가이드에서는 Capacitor 8에서 사용 가능한 옵션을 비교하고, 각 옵션에 대해 작동하는 code 예시를 보여주며, 사용자 데이터를 잃지 않고 한 옵션에서 다른 옵션으로 전환하는 방법을 설명합니다.

대부분의 앱은 결국 앱 상태용 Preferences나 SQLite와 인증 토큰용 secure storage, 두 가지를 함께 사용합니다.

옵션 한눈에 보기

localStorage / IndexedDB @capacitor/preferences 빠른 SQL KeyValueStore @capgo/capacitor-data-storage-sqlite @capgo/capacitor-native-biometric setData
기반 기술 WebView 저장소 UserDefaults / SharedPreferences SQLite 파일 SQLite 파일 Keychain / Keystore
OS 저장소 정리 후에도 유지됨 보장되지 않음 지원 지원 지원 지원
값의 타입 문자열 (localStorage) 문자열 JSON 값, 숫자, 불리언, Uint8Array 문자열 문자열
적정 크기 범위 작음 작음, 값당 몇 KB 큰 값, 많은 키 많은 키 매우 작음 (Android 약 8 KB 미만)
암호화 아니요 아니요 SQLCipher 선택 가능, 보안 저장소의 키 사용 SQLCipher 선택 가능, 설정 내 패스프레이즈 예, 하드웨어 기반
네임스페이스 오리진별 group 옵션 store 옵션 테이블 키 이름
접두사로 쿼리 아니요 아니요 키 목록 filtervalues 아니요
웹 지원 localStorage SQLite Wasm (OPFS) IndexedDB 보안 데이터용 아님

왜 localStorage를 쓰면 안 될까요?

localStorage 및 IndexedDB는 Capacitor WebView 내부에서 동작하지만, 데이터는 앱의 native storage가 아닌 WebView에 귀속됩니다. 기기 저장 공간이 부족하면 OS가 WebView storage를 회수할 수 있으며, Capacitor 문서에서도 유실되면 안 되는 데이터의 저장소로 사용하는 것을 권장하지 않습니다. 또한 앱의 hostname이나 scheme을 변경하면 capacitor.config origin이 변경되어 WebView가 다른 빈 저장소를 가리키게 됩니다.

사라질 수 있는 캐시 용도로만 사용하세요. 그 외의 모든 데이터는 native 기반 옵션을 사용해야 합니다.

옵션 1: 간단한 설정을 위한 Capacitor Preferences

@capacitor/preferences 는 가벼운 key-value 데이터를 위한 공식 플러그인입니다. iOS에서는 UserDefaults에 저장하고, Android에서는 SharedPreferences에, 웹에서는 localStorage.

bun add @capacitor/preferences
bunx cap sync
import { Preferences } from '@capacitor/preferences';

interface Settings {
  theme: 'light' | 'dark' | 'system';
  fontScale: number;
}

const DEFAULTS: Settings = { theme: 'system', fontScale: 1 };

export async function loadSettings(): Promise<Settings> {
  const { value } = await Preferences.get({ key: 'settings' });
  return value ? { ...DEFAULTS, ...JSON.parse(value) } : DEFAULTS;
}

export async function saveSettings(settings: Settings): Promise<void> {
  await Preferences.set({ key: 'settings', value: JSON.stringify(settings) });
}

에 저장합니다. 사용 사례: 테마, 언어, onboarding 플래그, 마지막 선택 탭, feature toggles 등.

사용 금지 사례:

  • 보안 취약성. 데이터가 앱 샌드박스 내에 평문으로 저장되며, 기기 백업에도 포함됩니다.
  • 대용량 데이터. UserDefaults 및 SharedPreferences 파일 전체를 메모리에 로드합니다. 대용량 JSON 데이터는 읽기/쓰기 성능을 저하시킵니다.
  • 접두사(prefix)로 목록을 조회하거나 삭제해야 하는 키가 많을 때. keys() 모든 데이터를 반환하므로, JavaScript에서 직접 필터링해야 합니다.

Preferences.configure({ group: 'MyApp' }) 키가 저장되는 네임스페이스를 변경합니다. 기본 그룹은 CapacitorStorage.

옵션 2: SQLite 키-값 저장소

캐싱된 API 응답, 초안, 오프라인 큐 또는 수백 개의 항목을 저장한다면 SQLite가 더 적합합니다. 대용량 데이터를 처리할 수 있고, 쓰기 작업이 원자적(atomic)이며, 암호화도 가능합니다.

빠른 SQL KeyValueStore

@capgo/capacitor-fast-sql 다음이 포함됩니다: KeyValueStore SQLite 연결 위에 클래스를 구축합니다. 이를 통해 __kv_store 테이블을 생성하고 값의 타입을 유지하며, 하나의 데이터베이스에서 여러 개의 이름 지정 저장소를 지원합니다.

bun add @capgo/capacitor-fast-sql
bunx cap sync

Fast SQL을 사용하려면 두 가지 네이티브 설정(iOS의 Info.plist로컬 네트워킹 및 Android의 localhost cleartext 규칙)이 필요합니다. 자세한 내용은 Capacitor 앱에서 SQLite를 사용하는 방법.

import { KeyValueStore } from '@capgo/capacitor-fast-sql';

const cache = await KeyValueStore.open({ database: 'app', store: 'api-cache' });

// Objects, arrays, numbers, booleans, null and Uint8Array are stored as-is
await cache.set('user:42', { id: 42, name: 'Ada', roles: ['admin'] });
await cache.set('lastSync', Date.now());

const user = (await cache.get('user:42')) as { id: number; name: string } | null;
const exists = await cache.has('lastSync');
const allKeys = await cache.keys();

await cache.remove('user:42');
await cache.clear(); // only clears the 'api-cache' store
await cache.close();

API 요약에서 확인하세요: open(options), fromConnection(connection, store), set, get (값이 없을 경우 null 반환), has, remove, clear, keys, close.

꼭 알아두어야 할 두 가지 패턴:

하나의 데이터베이스 내 여러 저장소. 이 store 옵션이 키의 네임스페이스를 지정하므로, settings, drafts 및 api-cache 함께 공존할 수 있고 각각 독립적으로 비울 수 있습니다.

실제 테이블과 함께 쓰는 Key-value. 앱에서 이미 관계형 데이터에 Fast SQL을 쓰고 있다면, 두 번째 연결을 열지 말고 기존 연결을 재사용하세요:

import { FastSQL, KeyValueStore } from '@capgo/capacitor-fast-sql';

const db = await FastSQL.connect({ database: 'app' });
const drafts = await KeyValueStore.fromConnection(db, 'drafts');
// You own `db`: drafts.close() does not disconnect it

나중에 key-value blob이 쿼리하고 싶을 만큼 커지면(예: 일주일이 지난 모든 초안), 같은 데이터베이스의 제대로 된 테이블로 옮길 수 있습니다.

암호화된 키-값 저장소

KeyValueStore.open 다음과 동일한 옵션을 받습니다: FastSQL.connect, 다음을 포함하여 encrypted 및 encryptionKey iOS와 Android에서:

import { KeyValueStore } from '@capgo/capacitor-fast-sql';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';

async function getKey(): Promise<string> {
  const { isSaved } = await NativeBiometric.isDataSaved({ key: 'kv.key' });
  if (isSaved) return (await NativeBiometric.getData({ key: 'kv.key' })).value;
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  const key = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
  await NativeBiometric.setData({ key: 'kv.key', value: key });
  return key;
}

const secureStore = await KeyValueStore.open({
  database: 'secure-kv',
  encrypted: true,
  encryptionKey: await getKey(),
});

이를 위해서는 네이티브 빌드에 SQLCipher가 필요합니다. 설정 방법과 백업 복구 후 키를 잃어버렸을 때의 대처법은 다음에서 확인하세요: Capacitor에서 SQLite 데이터베이스 암호화하는 방법.

@capgo/capacitor-data-storage-sqlite

@capgo/capacitor-data-storage-sqlite SQLite 기반의 전용 key-value 플러그인입니다(원작자 Jean Pierre Quéau, 현재 Capgo에서 유지 관리). 값은 문자열로 저장되며, 스토어당 여러 테이블 생성, 접두사/접미사 필터링, JSON 가져오기 및 내보내기 등 유용한 기능을 제공합니다.

bun add @capgo/capacitor-data-storage-sqlite
bunx cap sync
import { CapgoCapacitorDataStorageSqlite as Store } from '@capgo/capacitor-data-storage-sqlite';

await Store.openStore({ database: 'appStore', table: 'settings' });

await Store.set({ key: 'locale', value: 'fr' });
await Store.set({ key: 'profile', value: JSON.stringify({ name: 'Ada' }) });

const { value } = await Store.get({ key: 'locale' });
const { result: hasProfile } = await Store.iskey({ key: 'profile' });
const { keys } = await Store.keys();

// Switch table inside the same store
await Store.setTable({ table: 'drafts' });
const { values } = await Store.filtervalues({ filter: 'note%' }); // values whose key starts with "note"

await Store.remove({ key: 'note-1' });
await Store.clear(); // clears the current table

filtervalues SQL 패턴을 키에 적용합니다( LIKE 키에 SQL 패턴을 적용합니다(note% 접두사 필터링, %note 접미사 필터링, 포함 여부 확인을 위한 일반 텍스트). 패턴이 SQL 문자열에 직접 삽입되므로, 반드시 제어 가능한 필터만 전달하고 사용자 입력값을 그대로 전달하지 마세요.

웹 환경에서는 다음을 통해 IndexedDB를 사용합니다: localforage별도로 설치해야 합니다:

bun add localforage

암호화 관련 안내: openStore({ encrypted: true, mode: 'secret' }) 다음에서 가져온 암호(passphrase)와 함께 SQLCipher를 사용합니다: capacitor.config 하위 plugins.CapgoCapacitorDataStorageSqlite.encryptionSecret. 이 암호는 앱 번들에 포함되며, 설정하지 않을 경우 플러그인의 기본값이 적용됩니다. 이는 단순한 파일 검사를 방지하는 용도일 뿐, 앱을 언팩하는 공격을 막아주지는 못합니다. 보안이 중요한 데이터는 위 Fast SQL 예시처럼 Keychain이나 Keystore의 키를 사용하세요.

빠른 SQL KeyValueStore vs data-storage-sqlite?

  • 다음을 선택하세요: 빠른 SQL KeyValueStore 수동 JSON 처리 없이 타입이 지정된 값을 원하거나, 바이너리 값, 런타임 암호화 키가 필요하거나, 이미 테이블용 Fast SQL을 사용 중인 경우입니다.
  • 다음을 선택하세요: data-storage-sqlite 테이블 및 필터 기능, JSON 내보내기/가져오기, 또는 Electron 지원이 필요한 경우입니다.

옵션 3: 시크릿용 Keychain 및 Keystore

인증 토큰, 리프레시 토큰, API 키 및 데이터베이스 암호화 키는 플랫폼 보안 저장소(secure storage)에 보관해야 합니다. @capgo/capacitor-native-biometric (8.6.0 이상 버전) iOS Keychain 및 Android Keystore 기반의 범용 키-값 API를 제공합니다:

bun add @capgo/capacitor-native-biometric
bunx cap sync
import { AccessControl, NativeBiometric } from '@capgo/capacitor-native-biometric';

// Silent read and write
await NativeBiometric.setData({ key: 'session.refreshToken', value: token });
const { value: refreshToken } = await NativeBiometric.getData({ key: 'session.refreshToken' });

// Require Face ID / Touch ID / fingerprint to read
await NativeBiometric.setData({
  key: 'wallet.pin',
  value: pin,
  accessControl: AccessControl.BIOMETRY_ANY,
});
const { value: secretPin } = await NativeBiometric.getSecureData({
  key: 'wallet.pin',
  reason: 'Confirm it is you',
});

await NativeBiometric.deleteData({ key: 'session.refreshToken' });

값의 크기를 작게 유지하세요. 플러그인 문서에 따르면 Android Keystore 기반 암호화는 값당 약 8KB 미만일 때 가장 잘 작동합니다. 보호해야 할 데이터가 많다면, 여기에 무작위 키 하나를 저장하고 이를 사용해 SQLite 데이터베이스를 암호화하세요. 관련 내용은 오프라인 토큰을 위한 보안 저장소 포스트를 참고하세요 토큰 처리에 대해 더 깊이 다룹니다.

iOS의 Keychain 항목은 앱을 삭제해도 남아 있을 수 있습니다. 앱을 새로 설치한 후 첫 실행 시 앱이 깨끗한 상태에서 시작해야 한다면 오래된 secret을 삭제하세요(삭제 시 제거되는 Preferences에 “first run done” 플래그를 저장하고, 이 플래그가 없을 때 Keychain 항목을 삭제).

선택: 의사결정 목록

  1. 비밀 정보인가요? 토큰, 비밀번호, PIN, 키 등은 Keychain 또는 Keystore 저장소를 사용하세요.
  2. 몇 가지 작은 설정값인가요? Preferences를 사용하세요.
  3. 항목이 많거나, 대용량 JSON, 바이너리 데이터, 또는 그룹 단위로 만료시키고 삭제하려는 캐시인가요? SQLite key-value 저장소를 사용하세요.
  4. 필터링, 정렬, 조인이 필요한가요? 그렇다면 더 이상 key-value 데이터가 아닙니다. 테이블을 만들고, 다음을 참고하세요. SQLite 가이드, 또는 다음과 같은 타입 안정화 레이어를 사용할 수 있습니다: Kysely.

저장소 간 기존 데이터 마이그레이션

기존의 localStorage 또는 Preferences에서 SQLite 저장소로 전환할 때는 앱 시작 시점에 한 번만 데이터를 복사하면 됩니다. 앱의 다른 부분에서 데이터를 읽기 전에 복사를 완료하고, 실행 여부를 기록해 두세요.

import { Preferences } from '@capacitor/preferences';
import { KeyValueStore, type KeyValueValue } from '@capgo/capacitor-fast-sql';

export async function migrateToSqliteStore(): Promise<KeyValueStore> {
  const kv = await KeyValueStore.open({ database: 'app', store: 'settings' });
  if (await kv.has('__migrated_v1')) return kv;

  const { keys } = await Preferences.keys();
  for (const key of keys) {
    const { value } = await Preferences.get({ key });
    if (value === null) continue;
    let parsed: KeyValueValue = value;
    try {
      parsed = JSON.parse(value) as KeyValueValue;
    } catch {
      // keep plain strings as strings
    }
    await kv.set(key, parsed);
  }

  // Also copy from localStorage if older versions used it
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (key && !(await kv.has(key))) {
      await kv.set(key, localStorage.getItem(key));
    }
  }

  await kv.set('__migrated_v1', true);
  // Remove the old copies only after a release or two, once you are confident
  return kv;
}

비밀 정보를 보안 저장소로 복사했다면 즉시 기존 저장소에서 삭제해야 합니다. 토큰을 Keychain으로 옮긴 후에도 Preferences에 남겨두면 보안 취약점이 그대로 유지됩니다.

이 code는 JavaScript에서 실행되므로, Capgo live update와 함께 배포할 수 있습니다. SQLite 플러그인 자체는 네이티브이므로, 이를 추가하는 릴리스에는 스토어 빌드가 필요합니다. 플러그인을 먼저 배포한 다음 마이그레이션 code를 진행하세요.

문제 해결

Preferences 값은 null 웹 업데이트 이후에 변경됩니다. Origin(포트, 호스트명 또는 스킴)이 변경되어, localStorage 다른 빈 저장소입니다.

빠른 SQL KeyValueStore.open iOS에서 오류가 발생합니다. 로컬 HTTP 채널이 차단되었습니다. 다음을 추가하고 NSAllowsLocalNetworking 대상 Info.plist Native Build를 다시 수행하세요.

get data-storage-sqlite에서 객체 대신 문자열을 반환합니다. 해당 플러그인은 문자열만 저장할 수 있습니다. JSON.parse 값(value).

새 기기에서 백업 복원 후 Secret이 누락되었습니다. Keystore 키와 기기 전용 Keychain 항목은 새 하드웨어로 이전되지 않습니다. 보안 저장소는 재로그인을 통해 다시 가져올 수 있는 자격 증명 캐시로 취급하세요.

iOS에서 앱 재설치 후 설정은 사라졌지만 토큰은 남아 있음. 기대 결과: Preferences는 앱과 함께 삭제되어야 하며, Keychain 항목은 유지될 수 있음. 위에서 설명한 first-run flag 패턴을 사용하세요.

요약

  • Preferences: 작고 민감하지 않은 설정값.
  • SQLite key-value store (빠른 SQL) KeyValueStore 또는 @capgo/capacitor-data-storage-sqlite): 많은 양의 데이터나 큰 값, 캐시, 초안, 선택적 암호화용.
  • Keychain 또는 Keystore (@capgo/capacitor-native-biometric setData): 비밀 정보 및 암호화 키용.

관련 플러그인: Fast SQL 문서, data-storage-sqlite 문서, 그리고 네이티브 생체 인식 플러그인 페이지.

Capacitor 앱용 Live updates

웹 레이어 버그가 라이브 상태라면, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상 리뷰 절차를 그대로 따르는 반면, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 정말 프로페셔널한 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.