DuckDB-Wasm 인스턴스화
DuckDB-Wasm 인스턴스화 (Instantiate)
DuckDB-Wasm은 애플리케이션이 자산(asset)을 어떻게 번들링하고 서빙하는지에 따라 여러 방식으로 인스턴스화할 수 있어요. 각 방식은 브라우저의 능력에 맞춰 mainModule(WebAssembly 파일)과 mainWorker(워커 스크립트)를 결정하고, 이를 AsyncDuckDB에 넘겨줘요.
출처: 문서
본문
이 페이지는 jsDelivr CDN, webpack, Vite, 정적 서빙 파일에 대한 패턴을 보여줘요. 인스턴스화되면 db 객체는 데이터 가져오기와 쿼리 실행에 사용돼요.
번들 선택 (Bundle Selection)
DuckDB-Wasm은 서로 다른 브라우저 기능 집합을 위해 컴파일된 여러 WebAssembly 모듈을 제공해요. post-MVP WebAssembly 기능은 브라우저에 따라 지원 속도가 다르고, 각각이 일정한 성능 향상을 가져올 수 있기 때문이에요. selectBundle 함수는 동적 브라우저 검사를 실행해 현재 브라우저가 지원하는 가장 빠른 번들을 골라줘요.
mvp— WebAssembly 1.0 (MVP) 기준. 모든 곳에서 지원돼요.eh— Wasm 수준의 예외 처리를 추가해 성능을 높여요. DuckDB와 DuckDB-Wasm은 C++로 작성되었고 예외를 사용해 오류를 전파하므로, 네이티브 예외 처리가 JavaScript를 통한 에뮬레이션을 피하게 해 줘요.coi— 병렬 쿼리 실행을 위한 스레딩을 추가해요. 페이지가 cross-origin isolated 상태여야 하며, 명시적으로 선택해야 해요 (Threading 참고).
아래 예시들은 모두 selectBundle을 호출해서 브라우저가 실행할 수 있는 가장 빠른 번들을 받도록 해요. 선택된 번들과 기능 집합은 web shell에서 .features 명령으로 확인할 수도 있어요.
cdn(jsdelivr)
DuckDB-Wasm을 로드하는 가장 간단한 방법은 빌드 단계나 번들러 설정 없이 CDN에서 바로 로드하는 거예요. getJsDelivrBundles()는 jsDelivr에 호스팅된 번들 집합을 반환하고, selectBundle이 브라우저에 맞는 번들을 골라줘요. Web Worker 스크립트는 같은 출처(same-origin)여야 하므로, CDN 워커 URL을 importScripts하는 Blob으로 감싸고, 워커가 시작되면 임시 객체 URL을 해제해요.
import * as duckdb from '@duckdb/duckdb-wasm';
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
// Browser checks를 바탕으로 번들 선택
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker}");`], {type: 'text/javascript'})
);
// DuckDB-Wasm의 비동기 버전 인스턴스화
const worker = new Worker(worker_url);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(worker_url);
webpack
애플리케이션이 webpack으로 빌드된다면, CDN에서 가져오는 대신 번들러가 DuckDB-Wasm의 자산을 해석하고 내보내도록 해요. 각 .wasm 모듈을 직접 import하고, 워커 스크립트는 new URL(..., import.meta.url)로 참조해서 webpack이 지문(fingerprint)을 찍고 경로를 내보낸 파일로 다시 쓰게 해요. 번들의 최종 위치는 빌드 후에만 알 수 있으므로 번들을 수동으로 선언하고, selectBundle이 런타임에 mvp와 eh 변형 중에서 선택해요.
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm';
import duckdb_wasm_next from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: duckdb_wasm,
mainWorker: new URL('@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js', import.meta.url).toString(),
},
eh: {
mainModule: duckdb_wasm_next,
mainWorker: new URL('@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js', import.meta.url).toString(),
},
};
// Browser checks를 바탕으로 번들 선택
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// DuckDB-Wasm의 비동기 버전 인스턴스화
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
vite
Vite는 자산을 조금 다르게 해석해요. import에 ?url 접미사를 붙이면 Vite가 내용 대신 자산의 최종 URL을 반환해요. .wasm 모듈과 워커 스크립트를 모두 이 방식으로 import하고, 수동 번들 정의에 모아서 selectBundle에 런타임에 적절한 변형을 선택하게 해요.
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url';
import mvp_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url';
import duckdb_wasm_eh from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url';
import eh_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: duckdb_wasm,
mainWorker: mvp_worker,
},
eh: {
mainModule: duckdb_wasm_eh,
mainWorker: eh_worker,
},
};
// Browser checks를 바탕으로 번들 선택
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// DuckDB-Wasm의 비동기 버전 인스턴스화
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
정적 서빙 (Statically Served)
CDN이나 번들러에 의존하고 싶지 않다면 DuckDB-Wasm 파일을 직접 호스팅할 수 있어요. <https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/>에서 배포 파일을 수동으로 다운로드해 자신의 출처에서 서빙하고, 번들 경로를 그 위치로 지정해요. 이렇게 하면 모든 자산을 자신의 서버에 두게 돼서, 오프라인, air-gapped, 또는 엄격한 콘텐츠 보안 정책 배포에 적합해요. 아래의 플레이스홀더 경로를 파일을 서빙하는 위치에 맞게 바꾸세요.
import * as duckdb from '@duckdb/duckdb-wasm';
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
mvp: {
mainModule: 'change/me/../duckdb-mvp.wasm',
mainWorker: 'change/me/../duckdb-browser-mvp.worker.js',
},
eh: {
mainModule: 'change/me/../duckdb-eh.wasm',
mainWorker: 'change/me/../duckdb-browser-eh.worker.js',
},
};
// Browser checks를 바탕으로 번들 선택
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
// DuckDB-Wasm의 비동기 버전 인스턴스화
const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
설정 (Configuration)
instantiate()는 기본 설정으로 DuckDB-Wasm을 시작해요. 데이터베이스 동작을 바꾸려면 커넥션을 열기 전에 db 객체에 open()을 호출해서 설정 객체를 넘겨요.
import * as duckdb from '@duckdb/duckdb-wasm';
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
await db.open({
path: ':memory:',
query: {
castBigIntToDouble: true,
castDecimalToDouble: true,
castDurationToTime64: true,
castTimestampToDate: true,
queryPollingInterval: 1000,
},
});
const conn = await db.connect();
설정 객체의 자주 쓰이는 필드:
path: 열 데이터베이스 파일. 기본값은 인메모리 데이터베이스(:memory:).opfs://경로를 사용해서 데이터를 브라우저의 Origin Private File System에 영속화할 수 있어요 (Persistence with OPFS 참고).accessMode:duckdb.DuckDBAccessMode.READ_ONLY또는duckdb.DuckDBAccessMode.READ_WRITE.allowUnsignedExtensions: 서명되지 않은 확장의 로딩 허용 (Load Extensions 참고).maximumThreads: 사용할 스레드 수. cross-origin-isolated 페이지에서 스레드coi번들에만 적용돼요 (Threading 참고).query: 쿼리 결과가 Arrow에서 JavaScript 값으로 변환되는 방식을 제어해요.
query 객체는 Arrow-to-JavaScript 타입 매핑을 조정해요.
castBigIntToDouble: 64비트 정수를BigInt대신 JavaScript 숫자로 반환. 편리하지만 큰 정수는 정밀도를 잃을 수 있어요.castDecimalToDouble:DECIMAL값을 DuckDB의 정확한 10진수 표현 대신 부동소수점 숫자로 반환.castTimestampToDate:TIMESTAMP값을 JavaScriptDate객체로 반환.castDurationToTime64: duration 값을 64비트(마이크로초) 시간 값으로 반환.queryPollingInterval: 스트리밍 쿼리가 결과를 폴링하는 간격(밀리초).
스레딩 (Threading)
기본적으로 DuckDB-Wasm은 단일 스레드에서 실행돼요. 스레드 coi 번들은 여러 스레드에서 쿼리를 실행하지만 두 가지 요구사항이 있어요. 페이지가 cross-origin isolated 상태여야 하고, getJsDelivrBundles()는 mvp와 eh 번들만 반환하므로 selectBundle에 넘기는 집합에 coi 번들을 추가해야 해요. coi 번들은 보통의 모듈과 워커 외에도 세 번째 아티팩트인 pthread 워커가 필요해요.
import * as duckdb from '@duckdb/duckdb-wasm';
const DIST = 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/';
// getJsDelivrBundles()는 mvp와 eh만 반환하므로 coi를 명시적으로 추가
const bundles: duckdb.DuckDBBundles = {
...duckdb.getJsDelivrBundles(),
coi: {
mainModule: `${DIST}duckdb-coi.wasm`,
mainWorker: `${DIST}duckdb-browser-coi.worker.js`,
pthreadWorker: `${DIST}duckdb-browser-coi.pthread.worker.js`,
},
};
// selectBundle은 Wasm 예외, SIMD, 스레드를 지원하는 cross-origin-isolated
// 페이지에서만 coi를 선택하고, 그렇지 않으면 eh 또는 mvp로 폴백
const bundle = await duckdb.selectBundle(bundles);
const worker = new Worker(bundle.mainWorker!);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
// 스레드 수 설정 (coi 번들에만 유효)
await db.open({ maximumThreads: 4 });
const conn = await db.connect();
selectBundle이 coi 요구사항을 충족하지 못하면 eh나 mvp 번들로 폴백하므로, 같은 코드가 어디서든 실행돼요. cross-origin isolated이 아닌 페이지에서는 단순히 단일 스레드로 실행되고 bundle.pthreadWorker는 null이 돼요. 스레드 번들을 배포할 때 두 가지를 기억하세요.
- 페이지는
Cross-Origin-Opener-Policy: same-origin과Cross-Origin-Embedder-Policy: require-corp헤더로 서빙되어야 해요. 이 헤더가 브라우저가SharedArrayBuffer를 노출하고 페이지를 cross-origin isolated로 보고하게 해요. Cross-Origin Isolation 참고. - 확장은 스레드 플랫폼용으로 빌드되어야 해요.
coi번들은wasm_threads확장 플랫폼을 사용하므로,wasm_mvp나wasm_eh전용으로 게시된 확장은 로드되지 않아요.
OPFS를 사용한 영속화 (Persistence with OPFS)
기본적으로 DuckDB-Wasm 데이터베이스는 메모리에 살아 있다가 페이지가 닫히면 사라져요. 페이지 새로고침과 세션에 걸쳐 데이터를 영속화하려면 open()에 opfs:// 스킴의 path를 줘서 브라우저의 Origin Private File System (OPFS)에서 데이터베이스를 열어요.
import * as duckdb from '@duckdb/duckdb-wasm';
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
await db.open({
path: 'opfs://duckdb.db',
accessMode: duckdb.DuckDBAccessMode.READ_WRITE,
});
const conn = await db.connect();
await conn.query(`CREATE TABLE t AS SELECT * FROM range(10) AS r(i)`);
// OPFS로 변경사항을 플러시해 새로고침 후에도 유지
await conn.query(`CHECKPOINT`);
나중 세션에서 같은 opfs:// 경로를 다시 열면 테이블이 복원돼요. SQL에서 직접 opfs:// 파일을 읽고 쓸 수도 있어요. opfs.fileHandling을 'auto'로 설정하면 DuckDB-Wasm이 문장에서 참조된 opfs:// 경로를 자동으로 등록해요.
await db.open({
path: 'opfs://duckdb.db',
accessMode: duckdb.DuckDBAccessMode.READ_WRITE,
opfs: { fileHandling: 'auto' },
});
// OPFS에 저장된 Parquet 파일을 읽고, CSV를 OPFS에 다시 쓰기
await conn.query(`CREATE TABLE t AS SELECT * FROM 'opfs://data.parquet'`);
await conn.query(`COPY (SELECT * FROM t) TO 'opfs://export.csv'`);
참고: OPFS 접근은 동기 파일 접근 핸들에 의존하는데, 브라우저는 이를 Web Worker 안에서만 노출해요. DuckDB-Wasm의 비동기 API는 이미 그 안에서 실행돼요. 다음 사항을 명심하세요.
CHECKPOINT를 호출해 디스크에 쓰기를 플러시하고, 파일은 한 번에 하나의 핸들만 보유할 수 있으므로 다른 커넥션이나 데이터베이스 인스턴스가 열기 전에db.dropFile()(또는db.dropFiles())로 등록된 파일을 내려주세요. 그리고 OPFS 안팎으로 파일을 이동하는 것은 아직 지원되지 않아요.
더 알아보기 (Learn more)
- 데이터 가져오기 — 파일 등록과 인스턴스화된 데이터베이스에 데이터 삽입.
- 쿼리 실행 — 여기서 만든
db객체에 대한 쿼리 실행. - 배포 — 번들이 참조하는 라이브러리, 워커, WebAssembly 컴포넌트 서빙.
- DuckDB Wasm 클라이언트 — 계층화된 API와 위 스니펫이 참조하는 예시들.