-- CSV 파일 읽기 (자동으로 헤더와 타입을 유추함)SELECT * FROM 'data.csv' LIMIT 10;-- 특정 조건으로 필터링 및 집계SELECT department, AVG(salary) AS avg_salary FROM 'employees.csv'GROUP BY department;-- 여러 개의 Parquet 파일 한 번에 읽기SELECT COUNT(*) FROM 'sales_2023_*.parquet';
Python 환경에서는 메모리에 있는 Pandas DataFrame이나 Arrow 객체를 테이블처럼 바로 쿼리할 수 있습니다.
import duckdbimport pandas as pd맞습니다! 아주 예리하십니다. Cloudflare Workers는 브라우저와 다른 런타임(V8 엔진)을 쓰고, 일반 브라우저에서 쓰는 Web Worker나 공유 메모리(`SharedArrayBuffer`), 동기식 I/O 등을 지원하지 않기 때문에 일반적인 `@duckdb/duckdb-wasm` 패키지로는 작동하지 않습니다.하지만 이 한계를 뚫어내기 위해, 개발자들이 Cloudflare Workers 환경에 맞춰 **동기식 처리를 비동기식(Asyncify)으로 변환하고 단일 스레드로 최적화한 전용 WASM 빌드**들을 만들어 두었습니다 (대표적으로 `@ducklings/workers` 같은 프로젝트가 있습니다).이를 활용해 **Cloudflare Workers + R2 저장소 + DuckDB WASM**을 조합하는 상세 실전 메뉴얼을 공유해 드립니다.---### ⚠️ 1. 구현 전 필수 체크 리스트* **유료 플랜 필수 (Paid Plan):** Cloudflare Workers의 무료 플랜은 업로드할 수 있는 코드 및 에셋 크기가 **3MB**로 제한됩니다. 하지만 Workers용 DuckDB WASM 파일은 압축해도 약 **9.7MB**에 달합니다. 따라서 월 $5의 유료 플랜(최대 10MB 허용)을 구독하셔야만 배포가 가능합니다.* **메모리 제한:** Workers는 인스턴스당 메모리가 **128MB**로 엄격하게 제한됩니다. 너무 방대한 양의 데이터를 통째로 메모리에 올리는 쿼리는 실행 시 Worker가 터질 수 있으므로, 적절한 크기의 파일이나 요약된 Parquet 파일을 다루는 것이 좋습니다.* **데이터 적재(R2):** Workers 자체는 상태가 없는(Stateless) 서버리스 환경이므로 데이터를 저장할 수 없습니다. 분석할 데이터는 Cloudflare의 S3 호환 저장소인 **R2**에 올려두고 HTTP를 통해 읽어야 합니다.---### 📦 2. 패키지 설치Workers 전용으로 빌드된 DuckDB WASM 패키지를 설치합니다.```bashnpm install @ducklings/workers
만약 데이터를 인터넷 전체에 공개하고 싶지 않다면, S3 호환 API 규격을 활용해 DuckDB 내부에서 Access Key를 주입하여 프라이빗 R2 버킷의 파일을 읽어야 합니다.
// 쿼리 실행 전 S3 호환 시크릿을 먼저 생성합니다.await conn.query(` CREATE SECRET ( TYPE s3, KEY_ID '${env.R2_ACCESS_KEY_ID}', SECRET '${env.R2_SECRET_ACCESS_KEY}', REGION 'auto', ENDPOINT 'https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com' );`);// 이후 s3:// 프로토콜을 사용해 안전하게 비공개 파일을 가져옵니다.const sql = `SELECT * FROM 's3://my-private-bucket/data.parquet' LIMIT 10;`;const result = await conn.query(sql);
보안 팁:R2_ACCESS_KEY_ID 및 R2_SECRET_ACCESS_KEY와 같은 자격 증명은 절대 코드에 하드코딩하지 마시고, Wrangler의 wrangler secret put 명령어를 이용해 환경 변수로 주입해 주어야 안전합니다.
마지막 단계 제안:
이렇게 구성하시면 빵빵한 서버 인프라 없이도 Cloudflare의 엣지 서버(Edge) 단에서 엄청난 속도의 SQL 분석 환경을 구축할 수 있습니다!
혹시 이 Workers 코드를 프로젝트에 올려서 배포할 때 필요한 wrangler.toml 환경 설정 파일이나, 번들링을 돕기 위한 Vite 플러그인 세팅이 추가로 필요하신가요? 배포까지 완벽하게 마무리하실 수 있도록 이어서 안내해 드릴 수 있습니다.1. Pandas 데이터프레임 생성
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35]
})
#### ③ 결과 데이터를 파일로 내보내기분석이 끝난 데이터를 빠르고 쉽게 다른 포맷으로 저장할 수 있습니다.```sql-- 쿼리 결과를 CSV 파일로 저장COPY (SELECT * FROM my_table WHERE age > 30) TO 'output.csv' (HEADER, DELIMITER ',');-- 쿼리 결과를 Parquet 파일로 저장 (가장 추천하는 방식)COPY my_table TO 'output.parquet' (FORMAT PARQUET);
수십 GB의 CSV 파일에서 특정 조건의 데이터만 뽑아 Parquet이나 다른 CSV로 변환할 때 매우 유용합니다.
-- 1. CSV에서 데이터를 필터링하여 새로운 CSV로 저장COPY ( SELECT * FROM 'huge_data.csv' WHERE status = 'ACTIVE') TO 'active_data.csv' (HEADER, DELIMITER ',');-- 2. CSV를 빠르고 압축률이 좋은 Parquet 포맷으로 변환COPY ( SELECT * FROM 'huge_data.csv') TO 'compressed_data.parquet' (FORMAT PARQUET);
.output 명령어를 사용하면 이후에 실행되는 쿼리의 결과가 터미널 화면이 아닌 파일로 저장됩니다.
D> .output report.txt -- 지금부터 출력은 report.txt로 보냄D> SELECT department, COUNT(*) FROM 'employees.csv' GROUP BY department;D> .output stdout -- 다시 터미널 화면으로 출력 방향 복구
DuckDB에 직접 들어가지 않고, 외부 터미널 명령어나 쉘 스크립트에서 단일 쿼리를 실행하고 결과만 바로 받아볼 수 있습니다. 자동화 스크립트 작성에 필수입니다.
# 옵션 '-c'를 사용하여 쿼리 하나만 실행하고 빠져나오기duckdb -c "SELECT count(*) FROM 'data.csv';"# 결과를 CSV 형태로 출력해서 다른 터미널 명령어(grep, awk 등)로 넘기기duckdb -csv -c "SELECT * FROM 'data.csv';" | grep "Alice"# SQL 파일에 있는 쿼리를 실행하기duckdb my_database.db < query.sql
SELECT * FROM read_csv( 'data.csv', delim = ',', -- 구분자 (쉼표, 탭 등) header = true, -- 첫 번째 줄을 컬럼명으로 사용 nullstr = 'NA', -- null 값으로 취급할 문자열 columns = { -- 명시적으로 데이터 타입 지정 (성능 향상 및 에러 방지) 'id': 'INTEGER', 'name': 'VARCHAR', 'joined_date': 'DATE' });
텍스트 파일의 각 줄을 하나의 긴 문자열 컬럼으로 읽어 들인 후, SQL의 정규식 함수로 필요한 정보만 추출하는 가장 강력한 방법입니다.
-- 1. 텍스트 파일을 'line'이라는 단일 컬럼을 가진 테이블로 읽기WITH raw_logs AS ( SELECT * FROM read_csv( 'app.log', delim = '\n', -- 줄바꿈을 구분자로 써서 한 줄씩 읽음 columns = {'line': 'VARCHAR'}, -- 컬럼명은 line, 타입은 문자열 header = false ))-- 2. 정규식을 이용해 IP와 로그 레벨 추출하기SELECT -- 정규식 그룹()에 매칭된 첫 번째(1), 두 번째(2) 값을 가져옴 regexp_extract(line, '^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1) AS ip_address, regexp_extract(line, '\[(INFO|ERROR|WARN)\]', 1) AS log_level, line AS original_messageFROM raw_logs;
로그 분석을 할 때 DuckDB CLI 화면에 굳이 들어가지 않고, 터미널에서 한 줄의 명령어로 결과를 바로 파일로 뽑아내면 매우 편리합니다.
# 1. 10GB짜리 대용량 로그 파일에서 'ERROR'가 포함된 줄만 찾아 # IP별로 카운트한 결과를 터미널 표(box)로 즉시 확인duckdb -c ".mode box" -c " SELECT regexp_extract(line, '^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1) AS ip, count(*) as error_count FROM read_csv('app.log', delim='\n', columns={'line':'VARCHAR'}) WHERE line LIKE '%ERROR%' GROUP BY ip ORDER BY error_count DESC LIMIT 10;"
v7의 loader를 활용하여 컴포넌트가 렌더링되기 전에 서버에서 DuckDB 쿼리를 실행합니다.
import { useLoaderData } from "react-router";import duckdb from "duckdb";// ⚠️ 주의: 매 요청마다 DB를 새로 띄우면 성능이 저하되므로, // 전역 공간에 싱글톤 인스턴스로 선언하여 재사용하는 것이 좋습니다.const db = new duckdb.Database(":memory:"); // 혹은 영구 저장용 파일 경로 지정export async function loader() { return new Promise((resolve, reject) => { // 서버에 적재된 대용량 CSV나 로그 파일을 직접 쿼리 db.all( `SELECT category, COUNT(*) as cnt FROM 'server/data/large_logs.csv' GROUP BY category ORDER BY cnt DESC`, (err, rows) => { if (err) reject(err); resolve({ rows }); // 결과 데이터를 React 컴포넌트로 전달 } ); });}export default function AnalyticsPage() { // 타입 세이프하게 로더 데이터를 가져옵니다. const { rows } = useLoaderData<typeof loader>(); return ( <div className="p-6"> <h1 className="text-xl font-bold mb-4">서버에서 DuckDB로 집계한 결과</h1> <ul className="space-y-2"> {rows.map((row: any) => ( <li key={row.category} className="border-b pb-1"> <span className="font-semibold">{row.category}</span>: {row.cnt}개 </li> ))} </ul> </div> );}
Vite의 네이티브 모듈 번들링 에러
서버 사이드(loader 등)에서 Node.js용 duckdb 라이브러리를 사용할 때, Vite가 C++ 네이티브 바인딩을 해석하지 못해 에러를 뿜을 때가 많습니다.
이 경우 vite.config.ts에 아래 설정을 추가해 주시면 간단히 해결됩니다.
export default defineConfig({ ssr: { external: ['duckdb'] // Vite가 DuckDB를 번들링하지 않고 순수 Node 모듈로 두게 함 }});
파일명에 .server.를 붙이면 RRv7이 이 파일은 오직 서버 환경에서만 실행되는 파일임을 인지하여 번들 크기를 줄여줍니다.
import duckdb from 'duckdb';// 개발 모드에서 HMR(Hot Module Replacement) 시 DB가 계속 재성장하는 것을 방지declare global { var __duckdb: duckdb.Database | undefined;}// 싱글톤 인스턴스 반환if (!global.__duckdb) { // 메모리 모드: global.__duckdb = new duckdb.Database(':memory:'); // 파일 저장 모드: global.__duckdb = new duckdb.Database('server_data.db');}export const db = global.__duckdb;// 가독성을 위한 Promise 래퍼 함수export function queryAsync<T = any>(sql: string): Promise<T[]> { return new Promise((resolve, reject) => { db.all(sql, (err, rows) => { if (err) reject(err); else resolve(rows as T[]); }); });}
Workers 내부에 데이터를 쌓을 순 없으니(서버리스라 휘발됩니다), 데이터는 Cloudflare의 데이터 저장소인 **R2(S3 호환 스토리지)**에 Parquet이나 CSV 형태로 저장해 두고, Workers에 띄운 DuckDB가 필요할 때마다 R2 파일에 직접 쿼리를 날려 결과를 뽑아오는 아키텍처가 실무에서 가장 많이 쓰입니다.
Cloudflare Workers는 브라우저와 다른 런타임(V8 엔진)을 쓰고, 일반 브라우저에서 쓰는 Web Worker나 공유 메모리(SharedArrayBuffer), 동기식 I/O 등을 지원하지 않기 때문에 일반적인 @duckdb/duckdb-wasm 패키지로는 작동하지 않습니다.
하지만 이 한계를 뚫어내기 위해, 개발자들이 Cloudflare Workers 환경에 맞춰 동기식 처리를 비동기식(Asyncify)으로 변환하고 단일 스레드로 최적화한 전용 WASM 빌드들을 만들어 두었습니다 (대표적으로 @ducklings/workers 같은 프로젝트가 있습니다).
유료 플랜 필수 (Paid Plan): Cloudflare Workers의 무료 플랜은 업로드할 수 있는 코드 및 에셋 크기가 3MB로 제한됩니다. 하지만 Workers용 DuckDB WASM 파일은 압축해도 약 9.7MB에 달합니다. 따라서 월 $5의 유료 플랜(최대 10MB 허용)을 구독하셔야만 배포가 가능합니다.
메모리 제한: Workers는 인스턴스당 메모리가 128MB로 엄격하게 제한됩니다. 너무 방대한 양의 데이터를 통째로 메모리에 올리는 쿼리는 실행 시 Worker가 터질 수 있으므로, 적절한 크기의 파일이나 요약된 Parquet 파일을 다루는 것이 좋습니다.
데이터 적재(R2): Workers 자체는 상태가 없는(Stateless) 서버리스 환경이므로 데이터를 저장할 수 없습니다. 분석할 데이터는 Cloudflare의 S3 호환 저장소인 R2에 올려두고 HTTP를 통해 읽어야 합니다.
만약 데이터를 인터넷 전체에 공개하고 싶지 않다면, S3 호환 API 규격을 활용해 DuckDB 내부에서 Access Key를 주입하여 프라이빗 R2 버킷의 파일을 읽어야 합니다.
// 쿼리 실행 전 S3 호환 시크릿을 먼저 생성합니다.await conn.query(` CREATE SECRET ( TYPE s3, KEY_ID '${env.R2_ACCESS_KEY_ID}', SECRET '${env.R2_SECRET_ACCESS_KEY}', REGION 'auto', ENDPOINT 'https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com' );`);// 이후 s3:// 프로토콜을 사용해 안전하게 비공개 파일을 가져옵니다.const sql = `SELECT * FROM 's3://my-private-bucket/data.parquet' LIMIT 10;`;const result = await conn.query(sql);
보안 팁:R2_ACCESS_KEY_ID 및 R2_SECRET_ACCESS_KEY와 같은 자격 증명은 절대 코드에 하드코딩하지 마시고, Wrangler의 wrangler secret put 명령어를 이용해 환경 변수로 주입해 주어야 안전합니다.
name = "my-duckdb-worker"main = "src/index.ts"compatibility_date = "2024-01-01"# ⚠️ 유료 플랜(Standard) 설정 (월 $5)# 무료 플랜의 3MB 제한을 넘어 10MB까지 업로드 가능하게 합니다.compatibility_flags = [ "nodejs_compat" ][observability]enabled = true# R2 버킷 연결 (선택 사항)[[r2_buckets]]binding = 'MY_BUCKET'bucket_name = 'my-analytics-data'# 환경 변수 (비공개 S3 액세스 시 필요)[vars]R2_ACCESS_KEY_ID = "your_access_key"# R2_SECRET_ACCESS_KEY는 'wrangler secret put'으로 별도 관리 권장
데이터 저장: 대용량 로그(CSV/JSON)나 분석용 Parquet 파일을 Cloudflare R2에 저장합니다.
데이터 조회: 사용자가 API 요청을 보내면 Cloudflare Worker가 깨어납니다.
엔진 로드: Worker 내부의 DuckDB WASM 인스턴스가 (이미 떠 있지 않다면) 초기화됩니다.
연산 처리: DuckDB가 R2에 있는 파일의 필요한 부분(HTTP Range Request)만 읽어와서 SQL 연산을 수행합니다.
응답 반환: 최종 집계된 결과(JSON)만 클라이언트로 전송합니다.
이 구조의 최대 장점은 **"서버 유지비가 거의 들지 않으면서(Serverless), 기가바이트 단위의 데이터를 단 몇 초 만에 SQL로 분석할 수 있다"**는 점입니다.
마지막 팁:
배포 후 쿼리 속도가 느리다면, 원본 데이터를 Parquet 형식으로 변환하여 R2에 저장했는지 꼭 확인하세요. DuckDB는 Parquet의 메타데이터를 읽어 필요한 컬럼만 쏙쏙 골라 가져오기 때문에, 전체 파일을 다운로드하지 않아 Workers 환경에서 성능이 극대화됩니다.