Capacitor 앱에서 커스텀 SQLite 확장자를 사용할 수 있지만, 앱에 컴파일된 네이티브 code로만 가능합니다. iOS에서는 확장자를 정적으로 링크하고 모든 연결 시 init 함수를 호출해야 하며, Android에서는 확장자 로딩을 지원하는 SQLite 빌드를 번들링하고 CPU 아키텍처별 네이티브 라이브러리로 확장자를 배포해야 합니다. 이 가이드에서는 플랫폼 간 차이가 발생하는 이유를 설명하고, C로 작성된 동작하는 확장자를 제공하며, 다음 네이티브 변경 사항을 보여줍니다. @capgo/capacitor-fast-sql Capacitor 8에서
C 코드를 작성하기 전에 정말 필요한지 먼저 확인하세요. 이미 내장된 '확장 기능'이 많습니다.
SQLite 확장 기능이란 무엇인가
확장 기능은 SQLite 연결에 새로운 기능을 등록하는 네이티브 code입니다: 스칼라 함수(haversine_km(...)), 집계 함수, 콜레이션, 가상 테이블, FTS5 토크나이저 등이 있습니다. 모든 확장 기능은 하나의 진입점을 노출합니다:
int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
SQLite는 연결당 한 번 이 함수를 호출합니다. 등록한 것은 모두 해당 연결에서만 유효합니다.
모바일 앱에서 확장 기능을 직접 작성하는 일반적인 이유:
- 기본 토크나이저가 제대로 처리하지 못하는 언어를 위한
unicode61커스텀 FTS5 토크나이저, - 앱에서 필요한 수학 또는 지리 함수
WHERE및ORDER BY(거리 정렬, 스코어링), - 사용자가 기대하는 방식으로 이름을 정렬하는 콜레이션,
- 온디바이스 검색을 위한 벡터 검색 확장입니다.
이미 내장된 기능 확인하기
지원하는 가장 오래된 iOS 및 Android 버전에서 플러그인을 통해 다음을 실행하세요:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'probe' });
const [{ v }] = await db.query('SELECT sqlite_version() AS v');
const options = await db.query('PRAGMA compile_options');
const functions = await db.query('SELECT name FROM pragma_function_list ORDER BY name');
console.log(v, options.map((r) => r.compile_options));
확장이 필요 없는 일반적인 작업:
| 필요 기능 | 내장 기능 | 체크 항목 |
|---|---|---|
| 컬럼 내 JSON | JSON 함수 (json_extract, ->>) |
SQLite 3.38부터 내장. 구형 Android 시스템 SQLite에는 없을 수 있음 |
| 전문 검색 | FTS4, FTS5 | ENABLE_FTS4 / ENABLE_FTS5 에서 compile_options |
| 공간 바운딩 박스 | R*Tree | ENABLE_RTREE |
sin, cos, sqrt, pow |
수학 함수 | ENABLE_MATH_FUNCTIONS, SQLite 3.35+ |
모든 대상에 해당 기능이 있다면 여기서 멈추세요. Fast SQL의 웹 빌드는 JSON, FTS5, 수학 함수를 포함하는 공식 SQLite Wasm을 사용합니다.
iOS와 Android가 다르게 동작하는 이유
iOS. 시스템은 libsqlite3 으로 구성되며 SQLITE_OMIT_LOAD_EXTENSION, 따라서 load_extension() 은 존재하지 않습니다. App Store 규정도 새로운 code (지침 2.5.2)의 다운로드 및 실행을 금지하므로, 로딩하는 것은 .dylib 런타임에 하는 건 어차피 불가능합니다. 확장 기능은 앱 바이너리에 컴파일되어야 합니다. 많은 튜토리얼에서는 sqlite3_auto_extension() 앱 실행 시점에 호출하라고 합니다. 이에 의존하지 마세요. Xcode에 포함된 iOS SDK에서는, sqlite3.h 다음과 같이 선언합니다. API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...). 가장 확실한 방법은 각 연결 핸들이 열린 직후에 초기화 함수를 호출하는 것입니다.
Android. Android 프레임워크 내부의 SQLite (android.database.sqlite)에는 확장 기능을 로드할 API이 없습니다. 확장 기능을 로드할 수 있는 SQLite 빌드를 앱에 번들로 포함해야 합니다. requery/sqlite-android 는 프레임워크 클래스를 그대로 대체할 수 있고, 최신 SQLite(FTS5 및 JSON 포함)를 제공하며, 로드 가능한 확장 기능을 지원합니다 SQLiteCustomExtension. 확장 기능은 .so APK 내 ABI별 파일이 됩니다.
Web. 기본 SQLite Wasm 빌드는 네이티브 code를 로드할 수 없습니다. 확장이 컴파일된 커스텀 SQLite Wasm 빌드가 필요합니다.
현재 플러그인 지원 현황
| iOS | Android | 웹 | |
|---|---|---|---|
@capgo/capacitor-fast-sql 8.2 |
훅, 패치 불필요 | 프레임워크 SQLite 사용, 패치 필요 | 기본 SQLite Wasm |
@capacitor-community/sqlite 8.1 |
loadExtension() JS API에는 선언되어 있지만 네이티브로는 구현되지 않음 |
동일 | 미구현 |
따라서 현재 확장을 추가하려면 작은 네이티브 패치가 필요합니다. 범위가 한정된 변경이며, Bun를 통해 설치 전반에 계속 적용할 수 있습니다.
1단계: 확장 기능 작성
거리 함수가 좋은 예시입니다. 유용하고, 독립적이며, 검증하기 쉽습니다. 다음 파일로 저장하세요: geo.c:
#include <math.h>
#include <stddef.h>
#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1
static void haversine_km(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
(void)argc;
for (int i = 0; i < 4; i++) {
if (sqlite3_value_type(argv[i]) == SQLITE_NULL) {
sqlite3_result_null(ctx);
return;
}
}
const double earth_radius_km = 6371.0088;
const double to_rad = M_PI / 180.0;
double lat1 = sqlite3_value_double(argv[0]) * to_rad;
double lon1 = sqlite3_value_double(argv[1]) * to_rad;
double lat2 = sqlite3_value_double(argv[2]) * to_rad;
double lon2 = sqlite3_value_double(argv[3]) * to_rad;
double dlat = lat2 - lat1;
double dlon = lon2 - lon1;
double a = sin(dlat / 2) * sin(dlat / 2) +
cos(lat1) * cos(lat2) * sin(dlon / 2) * sin(dlon / 2);
sqlite3_result_double(ctx, 2 * earth_radius_km * asin(sqrt(a)));
}
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg;
return sqlite3_create_function_v2(
db, "haversine_km", 4,
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS,
NULL, haversine_km, NULL, NULL, NULL);
}
같은 파일이 두 가지 방식으로 빌드됩니다:
- 로드 가능한 확장으로 (Android):
SQLITE_EXTENSION_INIT1/2모든 SQLite 호출을pApiSQLite가 전달하는 포인터를 통해, 따라서.soSQLite에 링크되지 않습니다. - 정적 링크로
-DSQLITE_CORE(iOS): 매크로가 no-op이 되고 호출이 앱이 링크한 SQLite 라이브러리로 직접 전달됩니다.
SQLITE_DETERMINISTIC SQLite가 표현식 인덱스에서 해당 함수를 사용할 수 있게 합니다. SQLITE_INNOCUOUS 뷰와 트리거에서 안전하게 사용할 수 있음을 표시합니다.
macOS에서 이 파일을 두 가지 방식으로 컴파일했습니다: 정적 링크로 -DSQLITE_CORE 그리고 sqlite3_geo_init(db, NULL, NULL) 시스템 SQLite 연결에서 호출하고, 로드 가능한 라이브러리로 load_extension두 방식 모두 파리에서 런던까지 약 343.6km를 반환합니다.
2단계: iOS, 앱에 컴파일하고 연결별로 등록
앱 타겟에 소스 추가
- Xcode에서 드래그하여
geo.c로App그룹에 추가하고App타겟을 선택하세요. - Xcode가 브리징 헤더 생성을 제안합니다. 수락하거나
App/App-Bridging-Header.h를 만들고 Build Settings > Swift Compiler - General > Objective-C Bridging Header. - 다음에서 Build Phases > Compile Sources에서 더블클릭하고
geo.c에 파일별 플래그를 추가하세요-DSQLITE_CORE.
iOS SDK에는 두 가지가 포함됩니다 sqlite3.h 및 sqlite3ext.h, 따라서 추가 헤더는 필요 없습니다.
App-Bridging-Header.h:
#include <sqlite3.h>
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
open hook을 노출하도록 Fast SQL 패치
플러그인은 자체 모듈 안에서 연결을 열기 때문에 앱이 설정할 수 있는 훅이 필요합니다. Bun은 의존성을 패치하고 설치할 때마다 패치를 다시 적용할 수 있습니다:
bun patch @capgo/capacitor-fast-sql
편집 node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift. 파일 최상위에 공개 훅을 추가하세요:
public enum FastSqlExtensions {
/// Called with every new connection handle right after it opens.
/// Return SQLITE_OK (0) on success.
public static var onOpen: ((OpaquePointer) -> Int32)?
}
그런 다음 init(...), 암호화 블록 뒤, 다음 코드 앞에서 try applyPerformanceOptions(...), 호출하세요:
if let hook = FastSqlExtensions.onOpen {
let rc = hook(db)
if rc != SQLITE_OK {
throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
}
}
패치를 저장하세요:
bun patch --commit node_modules/@capgo/capacitor-fast-sql
Bun은 아래에 파일을 작성하고 patches/ 이를 해당 위치에 기록합니다 package.json 하위 patchedDependencies. 두 파일 모두 커밋하세요. 가이드에서 Capacitor 플러그인 패치 방법 이 워크플로를 더 자세히 다룹니다.
실행 시 확장 기능 등록
다음에서 ios/App/App/AppDelegate.swift, 플러그인 모듈을 import하고 다음에서 hook을 설정하세요: application(_:didFinishLaunchingWithOptions:):
import UIKit
import Capacitor
import CapgoCapacitorFastSql // CocoaPods module name. With SPM: import CapgoCapacitorFastSqlPlugin
// Inside the existing AppDelegate class:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FastSqlExtensions.onOpen = { db in
sqlite3_geo_init(db, nil, nil)
}
return true
}
hook이 플러그인의 open path 내부에서 실행되므로 WebView가 다시 로드된 후 열린 연결을 포함해 모든 연결에서 이 함수를 사용할 수 있습니다. 이를 다음 위치에 설정하면 didFinishLaunching JavaScript가 호출하기 전에 해당 함수가 준비되어 있음을 보장합니다 connect().
SQLCipher 빌드. 플러그인의 SQLCipher subspec을 사용하면 연결은 SQLCipher의 SQLite에 속하지만, 사용자의 -DSQLITE_CORE 파일은 시스템 라이브러리의 심볼과 링크될 수 있습니다. 배포 전에 이 조합을 명시적으로 테스트하거나, 확장 기능에 의존하는 데이터는 암호화되지 않은 데이터베이스에 보관하세요.
3단계: Android에서 확장을 로드할 수 있는 SQLite 번들링
각 ABI별로 확장 컴파일
Android NDK(Android Studio의 SDK Manager에서 설치)가 필요하며 sqlite3ext.h 및 sqlite3.h 에서 SQLite amalgamation 옆에 geo.c.
export NDK=$HOME/Library/Android/sdk/ndk/<version>
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64 # linux-x86_64 on Linux
export API=24 # your minSdkVersion
for pair in \
aarch64-linux-android:arm64-v8a \
armv7a-linux-androideabi:armeabi-v7a \
x86_64-linux-android:x86_64 \
i686-linux-android:x86
do
triple=${pair%%:*}
abi=${pair##*:}
mkdir -p android/app/src/main/jniLibs/$abi
$TOOLCHAIN/bin/clang --target=$triple$API \
-shared -fPIC -O2 \
-Wl,-z,max-page-size=16384 \
-o android/app/src/main/jniLibs/$abi/libgeo.so \
geo.c -lm
done
-Wl,-z,max-page-size=16384 16 KB 메모리 페이지를 사용하는 기기에 맞게 라이브러리를 정렬합니다. Google Play는 Android 15 이상을 대상으로 하는 앱에 16 KB 지원을 요구하므로, 관련 내용은 16 KB 페이지 크기와 Capacitor 플러그인. 앱이 64비트 기기에서만 배포된다면 두 개의 32비트 ABI를 제거할 수 있습니다.
라이브러리 이름은 libgeo.so이며, SQLite의 기본 진입점 이름은 sqlite3_geo_init입니다. 이는 C 함수와 일치합니다.
설치 시 네이티브 라이브러리 추출
최근 Android Gradle Plugin 버전은 .so 파일을 압축 해제하지 않고 APK 내부에 그대로 유지합니다. SQLite는 파일 경로로 확장자를 로드하므로 android/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
requery sqlite-android 추가
다음에서 android/build.gradle (루트)에서 압축 해제를 요청하고 JitPack을 추가하세요:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Fast SQL이 이를 사용하도록 패치
같은 Bun 패치 세션(bun patch @capgo/capacitor-fast-sql)을 열고 두 파일을 수정하세요.
node_modules/@capgo/capacitor-fast-sql/android/build.gradle, dependencies:
implementation 'com.github.requery:sqlite-android:3.49.0'
JitPack이 해당 저장소에 대해 빌드된 최신 버전을 사용하세요.
node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java: 프레임워크 import를 requery의 것으로 교체하고, 확장자를 포함하는 설정을 통해 열기.
// Replace:
// import android.database.sqlite.SQLiteDatabase;
// import android.database.sqlite.SQLiteStatement;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
import io.requery.android.database.sqlite.SQLiteDatabase;
import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration;
import io.requery.android.database.sqlite.SQLiteStatement;
public class SQLDatabase implements DatabaseConnection {
/** Set from MainActivity before the first connection opens. */
public static final java.util.List<SQLiteCustomExtension> extensions =
new java.util.concurrent.CopyOnWriteArrayList<>();
private SQLiteDatabase db;
private volatile boolean inTransaction = false;
public SQLDatabase(String path, boolean walMode, boolean performancePresets) throws Exception {
SQLiteDatabaseConfiguration config = new SQLiteDatabaseConfiguration(
path, SQLiteDatabase.CREATE_IF_NECESSARY);
config.customExtensions.addAll(extensions);
this.db = SQLiteDatabase.openDatabase(config, null, null);
SQLPerformanceConfig.apply(db::execSQL, walMode, performancePresets);
}
// ... the rest of the class stays as it is
}
requery의 클래스는 프레임워크 API (rawQuery, compileStatement, execSQL, 트랜잭션)를 그대로 반영하므로 파일의 나머지 부분은 수정 없이 컴파일됩니다. android.database.Cursor 은 그대로 유지됩니다. 패치를 다음으로 커밋하세요. bun patch --commit node_modules/@capgo/capacitor-fast-sql.
이 변경 사항은 암호화되지 않은 경로에만 적용됩니다. 암호화된 연결은 SQLCipher의 자체 클래스를 사용하며, 이 패치는 해당 부분을 건드리지 않습니다.
MainActivity에 확장 등록
package com.example.app;
import android.os.Bundle;
import app.capgo.capacitor.fastsql.SQLDatabase;
import com.getcapacitor.BridgeActivity;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
String path = getApplicationInfo().nativeLibraryDir + "/libgeo.so";
SQLDatabase.extensions.add(new SQLiteCustomExtension(path, "sqlite3_geo_init"));
super.onCreate(savedInstanceState);
}
}
전에 등록하여 super.onCreate WebView가 JavaScript를 실행하기 전에 목록이 채워지도록 합니다.
C 없이 Android: Java 함수
스칼라 함수만 필요하면 NDK 없이 Java로 작성된 함수를 requery에 등록할 수 있습니다. 패치된 SQLDatabase 생성자에서, 열기 후:
db.addFunction("haversine_km", 4, (args, result) -> {
double r = 6371.0088;
double lat1 = Math.toRadians(args.getDouble(0));
double lat2 = Math.toRadians(args.getDouble(2));
double dlat = lat2 - lat1;
double dlon = Math.toRadians(args.getDouble(3) - args.getDouble(1));
double a = Math.pow(Math.sin(dlat / 2), 2)
+ Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon / 2), 2);
result.set(2 * r * Math.asin(Math.sqrt(a)));
});
모든 호출이 JNI를 건너뛰어야 하므로 무거운 쿼리에서는 C보다 느리지만, 유지보수가 훨씬 간단합니다. FTS5 토크나이저와 가상 테이블은 여전히 C가 필요합니다.
4단계: JavaScript에서 사용하기
로컬에서 bunx cap sync 그리고 네이티브 리빌드:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'places' });
await db.execute(`CREATE TABLE IF NOT EXISTS places (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL
)`);
const nearby = await db.query(
`SELECT id, name, haversine_km(?, ?, lat, lon) AS km
FROM places
WHERE lat BETWEEN ? AND ? AND lon BETWEEN ? AND ?
ORDER BY km
LIMIT 20`,
[userLat, userLon, userLat - 0.5, userLat + 0.5, userLon - 0.7, userLon + 0.7],
);
이 BETWEEN box는 SQLite가 인덱스를 사용하도록 합니다 (lat, lon) 함수가 정확한 거리를 계산하기 전에 후보를 좁히기 위해. 없으면 모든 행에서 함수가 실행됩니다.
웹용 JavaScript 폴백
웹 빌드는 C code를 로드할 수 없으므로, 이를 감지하고 폴백하세요:
import { Capacitor } from '@capacitor/core';
export const hasGeoExtension = Capacitor.isNativePlatform();
// On the web: query the bounding box, then sort in JS with the same formula.
커스텀 FTS5 토크나이저
동일한 메커니즘으로 FTS5 토크나이저를 등록합니다. init 함수에서 fts5_api 포인터를 SELECT fts5(?1) 패턴으로 가져옵니다 FTS5 문서, 그런 다음 호출 xCreateTokenizer. 추가 요구사항 두 가지:
- SQLite 라이브러리에 FTS5가 포함되어야 합니다. requery 빌드에는 포함되어 있습니다. iOS에서는 다음을 확인하세요
PRAGMA compile_options위해ENABLE_FTS5. - FTS 테이블에 쓰거나 읽는 모든 연결에 토크나이저가 등록되어 있어야 합니다. FTS5는 테이블 정의에 토크나이저 이름을 저장하기 때문입니다. 이것이 연결별 훅이 중요한 이유입니다.
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');
배포 및 유지 관리
- 네이티브 재빌드 필요. 확장, 패치, Gradle 변경은 네이티브입니다. App Store와 Play Store용 새 빌드가 필요합니다. Capgo 빌드 클라우드에서 빌드와 서명을 모두 처리할 수 있습니다. 2026년 4월부터 iOS 업로드는 Xcode 26 이상으로 빌드해야 합니다.
- JavaScript는 여전히 빠르게 배포할 수 있습니다. 호출하는 SQL
haversine_km웹 번들에 포함되어 있으며 Live Update확장이 포함된 네이티브 빌드가 이미 설치되어 있는 한 가능합니다. 사용자가 구버전 바이너리를 실행할 수 있다면 네이티브 버전 체크로 새 SQL을 게이트하세요. - 플러그인 업그레이드. Fast SQL을 업데이트하면 Bun이 패치를 다시 적용합니다. 패치된 줄이 이동했다면 설치 과정이 조용히 훅을 제거하는 대신 명확하게 실패합니다. 새 버전에서 패치를 다시 생성하세요.
문제 해결
no such function: haversine_km iOS에서. 훅이 미리 설정되지 않았습니다 connect(), 또는 데이터베이스가 먼저 열렸습니다 didFinishLaunching 실행되었습니다. 훅 내부에 로그를 남겨 호출되는지 확인하세요.
내부 크래시 sqlite3_geo_init iOS에서. -DSQLITE_CORE 가 누락되어 파일이 로드 가능한 확장으로 컴파일되었고, 이 확장이 역참조하는 pApi 포인터이며, 이는 nil 여기에 있습니다. Compile Sources에서 파일별 플래그를 추가하세요.
no such function Android에서. 패치된 SQLDatabase 여전히 import하거나 android.database.sqlite.SQLiteDatabase, 첫 연결 이후에 확장이 목록에 추가된 경우입니다. 로그를 SQLDatabase.extensions.size() 연결 시 확인하세요.
dlopen failed: library "…/libgeo.so" not found. 기기 ABI용 라이브러리가 없습니다(대부분의 휴대폰은 arm64-v8a), 또는 네이티브 라이브러리가 추출되지 않았습니다. 확인하고 useLegacyPackaging Android Studio의 APK Analyzer로 APK를 검사하세요.
undefined symbol: sqlite3_geo_init. 전달된 entry point 이름이 SQLiteCustomExtension C 함수와 일치하지 않거나, 함수가 선언된 경우 static.
Play Console에서 16 KB 정렬에 대해 경고합니다. 파일 .so 을 -Wl,-z,max-page-size=16384 와 함께 최신 NDK로 다시 빌드하고, requery를 16 KB 정렬로 빌드된 버전으로 업데이트하세요.
해당 함수는 Android에서는 작동하지만 iOS 시뮬레이터에서는 작동하지 않습니다. 다음을 확인하세요: geo.c 의 멤버입니다 App 파일 트리뿐 아니라 target에도 존재해야 합니다.
요약
| 플랫폼 | 확장 기능이 포함되는 방식 | 등록 위치 |
|---|---|---|
| iOS | 다음으로 앱에 컴파일됨 -DSQLITE_CORE |
호출하는 플러그인 훅 sqlite3_geo_init 모든 새 연결 시 |
| Android | .so ABI별 jniLibs, requery sqlite-android에서 로드됨 |
SQLiteCustomExtension 각 연결이 열릴 때 읽는 목록 |
| 웹 | 기본 SQLite Wasm에서 지원 안 함 | JavaScript 폴백 |
먼저 내장 기능을 확인하세요. 그래도 확장이 필요하면 C로 한 번 작성하고, 네이티브 패치는 작게 유지하며, 지원하는 가장 오래된 기기에서 테스트하세요. 일반적인 플러그인 설정은 다음을 참고하세요: Capacitor 앱에서 SQLite를 사용하는 방법.