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 항목을 삭제).
선택: 의사결정 목록
- 비밀 정보인가요? 토큰, 비밀번호, PIN, 키 등은 Keychain 또는 Keystore 저장소를 사용하세요.
- 몇 가지 작은 설정값인가요? Preferences를 사용하세요.
- 항목이 많거나, 대용량 JSON, 바이너리 데이터, 또는 그룹 단위로 만료시키고 삭제하려는 캐시인가요? SQLite key-value 저장소를 사용하세요.
- 필터링, 정렬, 조인이 필요한가요? 그렇다면 더 이상 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-biometricsetData): 비밀 정보 및 암호화 키용.
관련 플러그인: Fast SQL 문서, data-storage-sqlite 문서, 그리고 네이티브 생체 인식 플러그인 페이지.