본문으로 바로가기

How to Use SQLite in a Capacitor App (2026 Guide)

SQLite를 사용하는 Capacitor 앱을 배울 수 있습니다: 플러그인을 선택하고 데이터베이스를 열고, 마이그레이션, 트랜잭션, 암호화 및 웹 지원을 실행하고, 작동하는 code.

SQLite를 사용하는 Capacitor 앱 (2026 년 안내서)

SQLite를 사용하는 Capacitor 앱에서 SQLite를 사용하려면 native SQLite 플러그인을 설치해야 합니다. @capgo/capacitor-fast-sqlSQLite를 사용하는 Capacitor 앱에서 SQLite를 사용하려면 native SQLite 플러그인을 설치해야 합니다.

SQLite를 사용하는 __CAPGO_KEEP_0__ 앱에서 SQLite를 사용하려면 native SQLite 플러그인을 설치해야 합니다.

SQLite를 사용하는 올바른 저장소 선택

Capacitor 앱은 데이터를 저장할 수 있는 여러 장소가 있습니다. 그들은 서로 다른 문제를 해결합니다:

저장소 SQLite SQLite
localStorage IndexedDB 임시 캐시 웹뷰 내부에 존재하며 OS가 이를 제거할 수 있습니다. localStorage에는 실제 쿼리가 없습니다.
@capacitor/preferences 몇 가지 작은 설정 (테마, 언어) 문자열 값만 지원하며, 큰 데이터를 저장하거나 암호화하지 않습니다.
키 체인 / 키 스토리지 토큰, 암호화 키 작은 비밀만
파일 (@capacitor/filesystem) 이미지, PDF, 내보내기 쿼리하지 않음
SQLite 파일 레코드, 오프라인 데이터, 동기화 큐, 검색

스키마와 마이그레이션 관리

Choosing a Capacitor SQLite plugin in 2026

2026년 Capacitor SQLite 플러그인 선택

@capgo/capacitor-fast-sql @capacitor-community/sqlite @capgo/capacitor-data-storage-sqlite
Purpose 목적 SQL 데이터베이스 SQLite 키-값 저장소
지원하는 플랫폼 iOS, 안드로이드, 웹 iOS, 안드로이드, 웹, Electron iOS, 안드로이드, 웹, Electron
웹 엔진 공식 SQLite Wasm + OPFS jeep-sqlite (sql.js + IndexedDB) IndexedDB
데이터 전송 지역 HTTP 채널, 브리지를 우회 Capacitor 브릿지 Capacitor 브릿지
암호화 선택적 SQLCipher (iOS, Android) SQLCipher는 항상 포함 SQLCipher, config에서 패스프레이즈
행 형식 열을 키로 하는 객체 열을 키로 하는 객체 문자열
ORM 지원 사용자 정의 드라이버 (Kysely, Drizzle) TypeORM 내장 드라이버 없음

이 안내서에서는 Fast SQL을 사용합니다. Fast SQL은 Capgo에서 관리되고 Capacitor 주요 버전을 추적합니다 (플러그인 v8은 Capacitor 8 버전을 지원합니다). 또한 SQLCipher는 옵트인으로 유지하고, 브리지를 통해 대형 결과 집합을 serialize하는 것을 피합니다. 이미 커뮤니티 플러그인을 실행 중이라면, 커뮤니티 플러그인과 Capgo의 비교 및 마이그레이션 안내서를 읽어보세요. Fast SQL 비교 및 마이그레이션 안내서 첫 번째로.

Fast SQL 설치

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

빠른 SQL은 작은 HTTP 서버를 시작하여 바인딩 된 localhost iOS: 루프백 네트워킹 허용

Fast SQL은 루프백 및 로컬 주소만을 위한 루프백 네트워킹만 허용합니다. 이는 실제 __CAPGO_KEEP_0__ 호출에 대한 App Transport Security를 완화하지 않습니다.

이것을 추가하세요 ios/App/App/Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
</dict>

NSAllowsLocalNetworking API

__CAPGO_KEEP_1__

생성 android/app/src/main/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">localhost</domain>
        <domain includeSubdomains="false">127.0.0.1</domain>
    </domain-config>
</network-security-config>

참조 android/app/src/main/AndroidManifest.xml:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

네트워크 보안 구성 (SSL 핀닝 등)을 이미 가지고 있다면, 추가할 파일 대신에 이 블록을 사용하세요. domain-config 데이터베이스를 열 때

데이터베이스 연결을 소유하는 단일 모듈을 생성하세요. 동일한 데이터베이스를 반복적으로 열면 비효율적이고, 마이그레이션을 실행하는 데 하나의 위치를 원합니다.

그리고

// src/db/index.ts
import { FastSQL, type SQLConnection } from '@capgo/capacitor-fast-sql';
import { migrate } from './migrate';

let dbPromise: Promise<SQLConnection> | null = null;

export function getDb(): Promise<SQLConnection> {
  if (!dbPromise) {
    dbPromise = (async () => {
      const db = await FastSQL.connect({
        database: 'notes',
        walMode: true, // PRAGMA journal_mode = WAL
        performancePresets: true, // synchronous=NORMAL, busy_timeout, foreign_keys=ON
      });
      await migrate(db);
      return db;
    })().catch((error) => {
      dbPromise = null; // let the next call retry instead of reusing the failure
      throw error;
    });
  }
  return dbPromise;
}

walMode and performancePresets 또한 SQLite에서 기본적으로 빠뜨리는 performancePresets 이러한 기능을 활성화합니다. foreign_keys실제로 작동합니다. ON DELETE CASCADE 호출

context getDb() 어디서든 데이터베이스가 필요할 때. 첫 번째 호출은 데이터베이스를 열고 마이그레이션하고, 나중에 호출은 동일한 프로미스를 재사용합니다. 데이터베이스를 열거나 마이그레이션하는 것이 실패하면 다음 호출은 다시 시도합니다.

PRAGMA user_version을 사용한 스키마 마이그레이션

Fast SQL은 raw SQL을 제공하며, 마이그레이션 프레임워크가 아닌 SQLite가 이미 파일 헤더에 버전 슬롯을 가지고 있기 때문에 괜찮습니다. PRAGMA user_version. ordered array에 순서가 있는 단계를 유지하고, 적용할 필요가 없는 단계만 적용합니다.

// src/db/migrate.ts
import type { SQLConnection } from '@capgo/capacitor-fast-sql';

// Index 0 = version 1, index 1 = version 2, ...
// Never edit a step after it shipped. Add a new one.
const steps: string[][] = [
  [
    `CREATE TABLE IF NOT EXISTS notes (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       title TEXT NOT NULL,
       body TEXT,
       created_at INTEGER NOT NULL
     )`,
  ],
  [
    `ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0`,
    `CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes (created_at)`,
  ],
];

export async function migrate(db: SQLConnection): Promise<void> {
  const rows = await db.query('PRAGMA user_version');
  const current = Number(rows[0]?.user_version ?? 0);

  for (let version = current; version < steps.length; version++) {
    await db.transaction(async (tx) => {
      for (const statement of steps[version]) {
        await tx.execute(statement);
      }
      // PRAGMA does not accept bound parameters, the value is a trusted integer
      await tx.execute(`PRAGMA user_version = ${version + 1}`);
    });
  }
}

이것을 안전하게 유지하는 규칙:

  • 한 SQL 문장만 호출합니다. native implementation은 문자열의 첫 번째 statement만 준비하기 때문에 execute() silent하게 하나를 실행합니다. "CREATE ...; CREATE ..." 각 버전은 트랜잭션 내에서 실행되므로, 중간에 충돌이 발생하면 이전 버전의 데이터베이스가 남아있고, 다음 런칭 시 단계를 다시 실행합니다.
  • 새로운 테이블이나 nullable 열을 추가하는 additive 변경을 선호합니다. SQLite는
  • SQLite는 새로운 테이블과 새로운 nullable 열을 지원합니다. ALTER TABLE ... DROP COLUMN and RENAME COLUMN 각 버전은 트랜잭션 내에서 실행되므로, 중간에 충돌이 발생하면 이전 버전의 데이터베이스가 남아있고, 다음 런칭 시 단계를 다시 실행합니다.

업데이트 및 라이브 업데이트

JavaScript 변경 사항을 함께 배송하면 Capgo 실시간 업데이트code 마이그레이션은 배포 패키지에 함께 배포됩니다. 이게 편리한데, 스키마 변경이 스토어 릴리즈가 필요하지 않기 때문입니다. 또한 롤백은 새로운 스키마 위에 이전 버전의 패키지를 올릴 수 있습니다. 마이그레이션은 역방향으로 호환되도록 유지하세요(열을 추가하되, 이름을 바꾸거나 삭제하지 마세요. 사용 중지하는 열을 삭제하는 릴리즈에서). 이전 버전의 패키지는 여전히 작동합니다.

데이터 읽기와 쓰기

SQLConnection 데이터 읽기와 쓰기를 위해 사용할 수 있는 네 가지 메소드가 있습니다:

메소드 반환 사용 방법
query(sql, params) SQLRow[] SELECT 및 PRAGMA 읽기
run(sql, params) { rowsAffected, insertId } INSERT, UPDATE, DELETE
execute(sql, params) { rows, rowsAffected, insertId } 어떤 데이터도, 전체 결과
executeBatch(ops) SQLResult[] 한 번의 라운드 트립에서 여러 statement을 사용합니다.

항상 사용자 입력으로부터 string concatenation을 사용하여 SQL을 빌드하지 말고 ? 값을 바인딩하는 placeholder를 사용하세요.

// src/db/notes.ts
import { getDb } from './index';

export interface Note {
  id: number;
  title: string;
  body: string | null;
  created_at: number;
  pinned: number;
}

export async function addNote(title: string, body: string | null): Promise<number> {
  const db = await getDb();
  const { insertId } = await db.run(
    'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
    [title, body, Date.now()],
  );
  if (insertId == null) throw new Error('Insert did not return an id');
  return insertId;
}

export async function listNotes(search = ''): Promise<Note[]> {
  const db = await getDb();
  const rows = await db.query(
    'SELECT id, title, body, created_at, pinned FROM notes WHERE title LIKE ? ORDER BY pinned DESC, created_at DESC',
    [`%${search}%`],
  );
  return rows as unknown as Note[];
}

export async function setPinned(id: number, pinned: boolean): Promise<void> {
  const db = await getDb();
  await db.run('UPDATE notes SET pinned = ? WHERE id = ?', [pinned ? 1 : 0, id]);
}

export async function deleteNote(id: number): Promise<number> {
  const db = await getDb();
  const { rowsAffected } = await db.run('DELETE FROM notes WHERE id = ?', [id]);
  return rowsAffected;
}

행은 열 이름으로 키가 지정된 평범한 객체로 돌아오므로, 열/값 매핑 단계가 필요하지 않습니다. 1 and 0 그리고 setPinned 변환합니다.

edge에서 데이터 레이어의 변환을 수행합니다.

SQLite가 동기화하는 방법과 같습니다.

import { getDb } from './index';

export async function importNotes(items: { title: string; body: string }[]) {
  const db = await getDb();
  const now = Date.now();

  await db.transaction(async (tx) => {
    await tx.executeBatch(
      items.map((item) => ({
        statement: 'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
        params: [item.title, item.body, now],
      })),
    );
  });
}

transaction() 트랜잭션과 batch writes beginTransaction(), commit() and rollback() 여러 단계의 쓰기를 wrap하는 트랜잭션을 사용하세요. 데이터가 일관되게 유지되고, SQLite가 커밋할 때 디스크에 동기화하는 것이 훨씬 빠르기 때문입니다.

모든 호출은 하나의 네이티브 연결을 공유합니다. 데이터베이스에 대한 다른 code가 열린 트랜잭션 내에서 실행됩니다. 트랜잭션을 짧게 유지하고 네트워크 요청을 기다리지 않도록 하세요.

이진 데이터 저장

Pass a Uint8Array 그리고 __CAPGO_KEEP_0__을 받습니다. 플러그인은 이진 데이터를 Base64로 인코딩하여 전송하고 실제 BLOB을 저장합니다. Uint8Array 대형 미디어(사진, 비디오)를 파일로 유지하고 SQLite에만 경로를 저장하세요. BLOB은 썸네일, 작은 첨부 파일 및 임베딩용으로 적합합니다.

const db = await getDb();
await db.execute('CREATE TABLE IF NOT EXISTS thumbnails (note_id INTEGER PRIMARY KEY, data BLOB)');
await db.run('INSERT OR REPLACE INTO thumbnails (note_id, data) VALUES (?, ?)', [noteId, bytes]);

const [row] = await db.query('SELECT data FROM thumbnails WHERE note_id = ?', [noteId]);
const image = row?.data as Uint8Array | undefined;

데이터베이스 암호화

Fast SQL은 iOS와 Android에서 SQLCipher를 지원합니다. 이 기능은 선택적이므로 필요하지 않은 앱은 이 기능을 포함하지 않습니다:

Android에서 앱의

const db = await FastSQL.connect({
  database: 'vault',
  encrypted: true,
  encryptionKey: keyFromSecureStorage,
});

에 net.zetetic:sqlcipher-android iOS에서 build.gradleiOS에서 pod을 switch합니다. CapgoCapacitorFastSql/SQLCipher subspec. Generating the key, storing it in the Keychain or Keystore, encrypting an existing plaintext database and rotating keys are covered step by step in SQLite 데이터베이스를 Capacitor에서 암호화하는 방법__CAPGO_KEEP_0__에서 암호화가 지원되지 않습니다.

웹 지원

웹에서 빠른 SQL은 공식 @sqlite.org/sqlite-wasm 파일을 Origin Private File System (OPFS)에서 저장합니다. 이 파일은 실제 SQLite 파일이며, 메모리에 복사되어 IndexedDB로 저장되는 것이 아닙니다.

OPFS는 교차 출처 격리(Cross-Origin Isolation)가 필요하므로, 개발 서버와 프로덕션 호스트 모두 두 개의 헤더를 보내야 합니다:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Vite에서:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  optimizeDeps: {
    exclude: ['@sqlite.org/sqlite-wasm'],
  },
});

헤더가 없으면 플러그인은 경고를 로깅하고 비영구 데이터베이스로 돌아갑니다: 리로드할 때까지 모든 것이 작동합니다. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) __CAPGO_KEEP_0__에서 기본값을 변경할 수 있습니다. native에서는 무시됩니다. connect()이 옵션은 교차 출처 이미지 및 스크립트도 차단합니다. CORP 또는 CORS 헤더를 보내지 않는 경우, 활성화할 때 테스트하세요.

require-corp 데이터베이스 파일이 어디에 저장되는지

데이터베이스 파일이 저장되는 위치

플랫폼 위치 주석
iOS Documents/<name>.db iCloud 및 장치 백업에 기본적으로 포함됩니다.
Android files/<name>.db 앱의 내부 저장소에 저장됩니다. Android Auto 백업 규칙에 따라
웹 페이지 출처에 대한 OPFS 사용자가 사이트 데이터를 삭제하면 삭제됩니다.

두 가지 실용적인 결과가 있습니다. 첫 번째로, 앱을 삭제하면 모바일 플랫폼 모두에서 파일이 삭제됩니다. 두 번째로, 백업은 새로운 장치에 데이터베이스 파일을 복원할 수 있습니다. 일반적으로 이것이 원하는 결과입니다. 그러나 백업이 이동하지 않는 암호로 암호화된 데이터베이스가 복원된 경우 복원된 파일을 열 수 없습니다. 암호화 가이드는 이에 대해 처리하는 방법을 설명합니다.

성능 체크리스트

  • 켜기 walMode. 글쓴이를 방해하는 독자들이 멈추세요.
  • 트랜잭션 내에서 batch writes를 수행하세요. executeBatch. 이메포트와 동기화에서 가장 큰 이익을 얻는 단일 승리입니다.
  • 필터링하거나 정렬하는 열에 색인을 추가하세요. 그런 다음 EXPLAIN QUERY PLAN.
  • 필요한 열만 선택하세요. 큰 TEXT 그리고 BLOB 열은 HTTP 채널을 통해 전송 시간을 비용으로 지불합니다.
  • 페이지네이션을 사용하세요. WHERE created_at < ? ORDER BY created_at DESC LIMIT 50 대신 OFFSET 대량
  • 데이터를 많이 삭제한 후에 VACUUM 가끔씩 파일 크기를 줄이기 위해

Troubleshooting

SQL execution failed Troubleshooting iOS에서 바로 연결한 후에 NSAllowsLocalNetworking 로컬 HTTP 요청이 차단되었습니다. Info.plist 가 올바른 위치에 있는지 확인하세요.

CLEARTEXT communication to localhost not permitted 안드로이드에서 사용합니다. Android에서

네트워크 보안 설정이 누락되거나 매니페스트에서 참조되지 않았습니다. 첫 번째 명령문만 실행되었습니다. executeBatch.

no such table 여러 명령문을 하나의 문자열로 넘겼습니다. 그들을 분리하세요, 또는 이 마이그레이션은 실행되지 않았거나 다른 데이터베이스 이름으로 실행되었습니다. 로그 PRAGMA user_version 시작 시 로그를 확인하고 스텝 배열과 비교하세요.

Android에서 boolean 또는 계산된 값에 대한 필터는 항상 일치하지 않습니다. Android 구현에서 읽기 쿼리에서 매개변수를 텍스트로 바인딩합니다. INTEGER 친화도 column에 대한 비교는 SQLite가 텍스트로 변환하기 때문에 작동하지만 표현식 (예: json_extract(...) = ? 또는 count(*) > ?Android에서 텍스트와 숫자를 비교하는 경우 1/0 대신 true/false대신 CAST(? AS INTEGER) 표현식과 비교할 때

INSERT ... RETURNING Android에서 반환되는 행이 없습니다. 현재 플러그인 버전에서 Android는 시작하는 statement가 특정한 경우에만 행을 반환합니다. SELECT, PRAGMA 또는 EXPLAIN. 사용하세요. insertId from run() 그리고 선택한 행을 나중에 사용하세요. 같은 방식으로 쿼리에서 사용하세요. WITH쿼리가 시작하는 문자가 ':' 인 경우, 쿼리를 ',' 로 시작하도록 wrapping하세요. SELECT또는 Android 에서 테스트하세요.

브라우저에서 다시 로드하면 데이터가 사라집니다. OPFS 가 사용되지 않는 경우, 거의 항상 COOP/COEP 헤더가 배포된 호스트에 없기 때문입니다.

Encryption is not available in this build iOS 에서 사용하세요. 성공했습니다. encrypted: true 더 나아가서

작은 앱에서는 Raw SQL 이 충분합니다. 몇 개의 테이블이 있는 경우, 타입화된 레이어가 도움이 됩니다:

Raw SQL 은 작은 앱에서 충분합니다. 몇 개의 테이블이 있는 경우, 타입화된 레이어가 도움이 됩니다.

__CAPGO_KEEP_0__의 플러그인 참조는 Fast SQL 문서 그리고 플러그인 페이지. Fast SQL이 필요할 때는 native rebuild가 필요합니다. 추가하거나 변경할 때마다. Capgo 빌드 can produce signed iOS and Android binaries in the cloud. Since April 2026 App Store uploads must be built with Xcode 26 or later, so check your build machine before the next release.

Capacitor 앱에 대한 즉시 업데이트

웹-layer 버그가 활성화된 경우, 앱 스토어 승인 대기 없이 Capgo를 통해 패치를 배포합니다. 사용자는 배경에서 업데이트를 받으며 네이티브 변경 사항은 일반적인 검토 경로를 따릅니다.

최신 블로그 글

Capgo은 전문적인 모바일 앱을 만들기 위해 필요한 최고의洞察력을 제공합니다.