Jace Docs

DuckDB

DuckDB

1. DuckDB의 핵심 특징

  • 설치 과정 Zero: 별도의 서버를 띄울 필요 없이 프로그램 내에서 바로 실행됩니다 (In-process).
  • OLAP 최적화: 데이터를 열(Column) 단위로 저장하고 처리하여, 집계 및 분석 쿼리가 매우 빠릅니다.
  • 직접 읽기(Direct Read): 데이터를 DB에 적재(Load)하지 않고도 CSV, Parquet, JSON 파일을 직접 쿼리할 수 있습니다.

2. 설치 방법

사용하시는 환경에 따라 설치가 매우 간단합니다.

Python 환경:

pip install duckdb

CLI 환경 (터미널):

  • DuckDB 공식 홈페이지에서 OS에 맞는 실행 파일을 다운로드 후 압축을 풀면 바로 실행 가능한 duckdb 파일이 나옵니다.
  • macOS (Homebrew): brew install duckdb

3. 실전 활용 가이드 (핵심 기능)

DuckDB의 진가는 외부 데이터를 다룰 때 나타납니다. 아래는 가장 많이 쓰이는 SQL 패턴들입니다.

① CSV 및 Parquet 파일 직접 쿼리하기

데이터를 테이블로 만들 필요 없이, 파일 경로를 테이블 이름처럼 사용합니다.

-- 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 완벽 연동

Python 환경에서는 메모리에 있는 Pandas DataFrame이나 Arrow 객체를 테이블처럼 바로 쿼리할 수 있습니다.

import duckdb
import 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 패키지를 설치합니다.

```bash
npm install @ducklings/workers

💻 3. Workers용 DuckDB 구현 예시 (TypeScript)

Workers 내에서 DuckDB를 사용할 때 가장 중요한 점은, 매번 요청이 들어올 때마다 WASM을 새로 파싱하고 초기화하면 응답 속도가 심각하게 떨어진다는 것입니다. 전역 변수를 활용해 한 번 생성한 DB 인스턴스를 재사용(Reuse)해야 합니다.

// src/index.ts
import { init, DuckDB } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';

// ⚠️ 전역 스코프에 변수를 선언하여 동일한 Isolates(인스턴스)로 
// 들어오는 요청들이 DB 인스턴스를 공유할 수 있게 합니다.
let db: DuckDB | null = null;

export default {
  async fetch(request: Request, env: any, ctx: any): Promise<Response> {
    try {
      // 1. DuckDB 인스턴스가 없다면 최초 1회 초기화
      if (!db) {
        db = await init(wasmModule);
      }
      
      // 2. 데이터베이스 커넥션 열기
      const conn = await db.connect();
      
      // 3. 쿼리 실행
      // Workers 전용 빌드에는 Parquet 및 httpfs 익스텐션이 내장되어 있습니다.
      // ⚠️ R2 버킷의 데이터를 쿼리할 때는 공개된 URL(또는 커스텀 도메인)을 사용합니다.
      const sql = `
        SELECT category, COUNT(*) as total
        FROM 'https://data.your-domain.com/sales_logs.parquet'
        GROUP BY category
        ORDER BY total DESC
        LIMIT 5;
      `;
      
      const queryResult = await conn.query(sql);
      
      // 4. 결과를 JSON으로 변환
      const results = queryResult.toArray().map(r => r.toJSON());
      
      // 5. 커넥션 종료 (Isolates가 유지되더라도 커넥션은 닫아주는 것이 안전합니다)
      await conn.close();
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' },
      });
      
    } catch (error: any) {
      return new Response(JSON.stringify({ error: error.message }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }
  },
};

🌐 4. Cloudflare R2 비공개 버킷에서 파일 읽기

만약 데이터를 인터넷 전체에 공개하고 싶지 않다면, 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_IDR2_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] })

2. DuckDB로 데이터프레임 직접 쿼리 (df라는 변수명을 그대로 사용)

result = duckdb.query("SELECT name FROM df WHERE age > 28").df()

print(result)

name

0 Bob

1 Charlie


#### ③ 결과 데이터를 파일로 내보내기
분석이 끝난 데이터를 빠르고 쉽게 다른 포맷으로 저장할 수 있습니다.

```sql
-- 쿼리 결과를 CSV 파일로 저장
COPY (SELECT * FROM my_table WHERE age > 30) TO 'output.csv' (HEADER, DELIMITER ',');

-- 쿼리 결과를 Parquet 파일로 저장 (가장 추천하는 방식)
COPY my_table TO 'output.parquet' (FORMAT PARQUET);

4. 유용한 CLI 명령어 (터미널 환경)

CLI에서 duckdb를 실행한 후 사용할 수 있는 주요 점(.) 명령어입니다.

  • .help : 모든 명령어 목록 보기
  • .tables : 현재 데이터베이스에 있는 테이블 목록 보기
  • .schema 테이블명 : 특정 테이블의 스키마(구조) 보기
  • .timer on : 쿼리 실행 시간 표시 켜기
  • .exit 또는 .quit : DuckDB 종료

CLI 환경

터미널(CLI) 환경에서 DuckDB를 사용하시기로 한 것은 탁월한 선택입니다! DuckDB의 CLI는 가볍고 빠르며, 복잡한 데이터 파이프라인이나 스크립트 없이도 터미널에서 즉각적인 데이터 분석을 가능하게 해주는 강력한 도구입니다.

CLI 환경에 맞춘 DuckDB 상세 실전 메뉴얼을 정리해 드립니다.


1. DuckDB 실행 방식 (메모리 vs 디스크)

터미널에서 duckdb를 입력하여 실행할 때, 뒤에 파일명을 붙이느냐 마느냐에 따라 동작 방식이 다릅니다.

  • 인메모리 모드 (휘발성): ```bash duckdb
    테스트용으로 가장 많이 쓰입니다. 데이터를 생성하고 가공해도 종료(`.exit`)하면 데이터가 모두 날아갑니다. 주로 외부 파일(CSV, Parquet)을 읽어서 조회만 할 때 유용합니다.
  • 디스크 모드 (영구 저장):
    duckdb my_database.db
    my_database.db라는 파일에 데이터를 저장합니다. SQLite처럼 하나의 파일이 곧 데이터베이스가 되며, 나중에 다시 접속해서 데이터를 조회할 수 있습니다.

2. CLI 출력 포맷 예쁘게 꾸미기 (필수 팁)

터미널에서 데이터를 조회할 때 표 형태가 깨지면 보기 불편합니다. DuckDB CLI는 다양한 출력 모드를 지원합니다. DuckDB 프롬프트(D>)에서 아래 명령어들을 입력해 보세요.

  • .mode box (가장 추천!): 데이터를 깔끔한 표(Box) 형태로 예쁘게 그려줍니다.
  • .mode markdown: 마크다운 표 형식으로 출력합니다. 결과를 복사해서 GitHub이나 문서에 붙여넣기 좋습니다.
  • .mode csv: 쿼리 결과를 CSV 형태로 터미널에 뿌려줍니다.
  • .headers on / .headers off: 컬럼 이름(헤더)을 표시할지 여부를 결정합니다.

사용 예시:

D> .mode box
D> SELECT * FROM 'data.csv' LIMIT 3;
┌───────┬───────┬────────┐
│  id   │ name  │  age   │
│ int32 │ varchar │ int32  │
├───────┼───────┼────────┤
1 │ Alice │     30
2 │ Bob   │     25
3 │ Carol │     35
└───────┴───────┴────────┘

3. 꼭 알아야 할 핵심 Dot(.) 명령어

CLI 환경을 제어하는 명령어들입니다.

  • .tables: 현재 DB에 생성된 테이블과 뷰 목록을 보여줍니다.
  • .schema [테이블명]: 테이블을 생성한 CREATE TABLE 문(구조)을 보여줍니다. 테이블명을 생략하면 전체 스키마를 보여줍니다.
  • .timer on: 쿼리가 실행되는 데 걸린 시간을 밀리초 단위까지 보여줍니다. 성능 측정에 필수적입니다.
  • .maxrows N: 화면에 출력될 최대 행의 수를 제한합니다. (기본적으로 DuckDB는 데이터가 많으면 자동으로 자릅니다)
  • .read script.sql: 외부에 작성해 둔 SQL 스크립트 파일을 통째로 읽어서 실행합니다.
  • .exit 또는 .quit: DuckDB를 종료하고 터미널로 돌아갑니다. (단축키: Ctrl + D)

4. 외부 파일 처리하기 (가져오기 & 내보내기)

CLI 환경에서 DuckDB를 쓰는 가장 큰 이유는 외부 데이터를 변환하고 추출하는 속도가 압도적이기 때문입니다.

① 파일의 쿼리 결과를 다른 파일로 바로 내보내기

수십 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       -- 다시 터미널 화면으로 출력 방향 복구

5. CLI 쉘 스크립트(Bash 등)와 연동하기

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

DuckDB CLI 환경에서 CSV, JSON, 그리고 텍스트(로그) 파일을 다루는 구체적인 방법

📊 1. CSV 파일 다루기

DuckDB는 CSV 파일의 헤더, 구분자(Delimiter), 데이터 타입을 자동으로 유추하는 인공지능형 파서(Parser)를 내장하고 있습니다.

① 기본 조회 및 자동 인식

가장 간단하게는 파일 경로를 따옴표(')로 감싸서 테이블처럼 사용하면 됩니다.

-- DuckDB가 알아서 헤더와 데이터 타입을 분석하여 읽음
SELECT * FROM 'data.csv' LIMIT 5;

② 수동 설정으로 정밀하게 읽기 (read_csv 함수)

자동 인식이 실패하거나, 특정 설정을 강제해야 할 때 사용합니다.

SELECT * FROM read_csv(
    'data.csv',
    delim = ',',            -- 구분자 (쉼표, 탭 등)
    header = true,          -- 첫 번째 줄을 컬럼명으로 사용
    nullstr = 'NA',         -- null 값으로 취급할 문자열
    columns = {             -- 명시적으로 데이터 타입 지정 (성능 향상 및 에러 방지)
        'id': 'INTEGER', 
        'name': 'VARCHAR', 
        'joined_date': 'DATE'
    }
);

③ 여러 CSV 파일 한 번에 묶어서 읽기 (와일드카드)

이름이 비슷한 여러 CSV 파일을 하나의 테이블처럼 조회할 수 있습니다.

-- logs_2026_01.csv, logs_2026_02.csv 등을 모두 합쳐서 조회
SELECT count(*) FROM 'logs_2026_*.csv';

🌐 2. JSON 파일 다루기

JSON은 웹 API나 NoSQL에서 많이 쓰이는 비정형 데이터입니다. DuckDB는 줄바꿈으로 구분된 JSON(NDJSON)과 일반 JSON 배열을 모두 완벽하게 지원합니다.

① 기본 조회

SELECT * FROM 'data.json' LIMIT 5;

② 중첩된(Nested) JSON 데이터 추출하기

JSON 내부의 객체나 배열에 접근할 때는 -> (JSON 객체 반환) 또는 ->> (텍스트로 반환) 연산자를 사용합니다.

예시 JSON 구조: {"id": 1, "user": {"name": "Alice", "age": 30}}

-- user 객체 안의 name 추출
SELECT 
    id, 
    user->>'name' AS user_name,
    (user->>'age')::INTEGER AS user_age  -- 숫자로 형변환
FROM 'data.json';

③ JSON 배열 풀기 (Unnest)

JSON 안에 배열이 들어있을 때, 이를 여러 행(Row)으로 쪼개서 정규화된 테이블처럼 만들 수 있습니다. 예시 JSON 구조: {"dept": "IT", "employees": ["Alice", "Bob"]}

SELECT 
    dept, 
    unnest(employees) AS employee 
FROM 'department.json';
-- 결과는 (IT, Alice), (IT, Bob) 총 2개의 행이 됩니다.

📝 3. 텍스트 및 로그(Log) 파일 다루기

로그 파일은 표준화된 형식이 없고 줄마다 텍스트가 나열되어 있어 다루기 까다롭습니다. DuckDB에서는 다음 두 가지 전략으로 로그를 파싱합니다.

전략 A: 한 줄씩 통째로 읽어서 정규식(Regex)으로 파싱하기

텍스트 파일의 각 줄을 하나의 긴 문자열 컬럼으로 읽어 들인 후, 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_message
FROM raw_logs;

전략 B: 구분자가 있는 로그 (공백, 파이프 등)

Apache나 Nginx 로그처럼 공백( )이나 특정 기호(|)로 데이터가 구분되어 있다면 read_csvdelim 옵션을 활용할 수 있습니다.

-- 구분자가 공백(' ')인 로그 읽기
SELECT * FROM read_csv(
    'access.log',
    delim = ' ',
    header = false,
    columns = {
        'ip': 'VARCHAR',
        'identity': 'VARCHAR',
        'userid': 'VARCHAR',
        'time': 'VARCHAR',
        'request': 'VARCHAR',
        'status': 'INTEGER',
        'size': 'INTEGER'
    }
);

💡 CLI 환경 꿀팁: 쉘(Bash) 명령어로 파이프라인 만들기

로그 분석을 할 때 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;
"

React Router v7과 DuckDB 조합하기

프레임워크로 진화한 React Router v7과 초고속 분석 DB인 DuckDB를 조합하시다니, 엄청난 성능 중심의 아키텍처를 구상하고 계시네요! 이 조합은 대용량 파일 가공이나 무거운 집계 데이터를 다룰 때 진가를 발휘합니다.

React Router v7은 서버(SSR)와 클라이언트(SPA/WASM) 영역을 모두 다루기 때문에, DuckDB를 어느 위치에서 실행할 것인가에 따라 접근 방법이 완전히 달라집니다. 상황에 맞게 쓰실 수 있도록 두 가지 방식 모두 정리해 드립니다.


📊 1. SSR(서버) vs WASM(클라이언트) 선택 가이드

구분서버 사이드 (SSR 모드)클라이언트 사이드 (WASM 모드)
작동 환경Node.js 서버 (Loaders / Actions)사용자의 웹 브라우저
추천 용량수백 MB ~ 수십 GB 이상의 대용량수십 MB 이하의 가벼운 데이터
장점사용자의 기기 성능에 의존하지 않음.서버 비용 제로, 프라이버시 보호(로컬 파일 처리)
사용 라이브러리duckdb (네이티브 패키지)@duckdb/duckdb-wasm

🌐 2. 서버 사이드 (SSR Mode) 활용법

가장 흔하게 쓰이는 강력한 패턴입니다. 사용자는 결과물만 가볍게 받아보고, 무거운 데이터 연산(CSV/Parquet 파싱 등)은 서버의 loaderaction에서 DuckDB가 전담합니다.

① 패키지 설치

npm install duckdb

② 라우트 파일 작성 (app/routes/analytics.tsx)

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>
  );
}

💻 3. 클라이언트 사이드 (WASM / SPA Mode) 활용법

만약 사용자가 브라우저에 직접 드롭한 대용량 CSV 파일을 브라우저 렉 없이 초고속으로 쿼리하게 만들거나, 오프라인 지원 SPA를 만들고 싶다면 WebAssembly(WASM) 버전을 사용해야 합니다.

① 패키지 설치

npm install @duckdb/duckdb-wasm

② 컴포넌트 작성 예시

브라우저 환경에서는 WASM 바이너리를 비동기로 로드해야 하므로 복잡해 보일 수 있습니다. 실무에서는 이를 커스텀 훅(Hook)이나 Context로 격리하는 것이 좋습니다.

import { useEffect, useState } from "react";
import * as duckdb from "@duckdb/duckdb-wasm";

export default function BrowserAnalytics() {
  const [results, setResults] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function initDuckDB() {
      try {
        // 1. CDN을 통해 WASM 번들 로드
        const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
        const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
        
        const worker = new Worker(bundle.mainWorker!);
        const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
        await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
        
        const conn = await db.connect();
        
        // 2. 외부 URL의 CSV를 브라우저 내 가상 파일로 등록
        await db.registerFileURL(
          'data.csv', 
          'https://raw.githubusercontent.com/.../data.csv', 
          duckdb.DuckDBDataProtocol.HTTP, 
          false
        );
        
        // 3. 쿼리 실행
        const res = await conn.query(`SELECT * FROM 'data.csv' LIMIT 5`);
        setResults(res.toArray().map(r => r.toJSON()));
        
        // 4. 리소스 정리
        await conn.close();
        await db.terminate();
      } catch (err) {
        console.error("DuckDB WASM 에러:", err);
      } finally {
        setLoading(false);
      }
    }
    
    initDuckDB();
  }, []);

  if (loading) return <div>DuckDB WASM 로딩 중...</div>;

  return (
    <div className="p-6">
      <h1 className="text-xl font-bold">브라우저 내 DuckDB WASM 실행 결과</h1>
      <pre className="bg-gray-100 p-4 rounded mt-2">
        {JSON.stringify(results, null, 2)}
      </pre>
    </div>
  );
}

🛠️ v7 환경에서 자주 겪는 트러블슈팅 꿀팁

Vite의 네이티브 모듈 번들링 에러 서버 사이드(loader 등)에서 Node.js용 duckdb 라이브러리를 사용할 때, Vite가 C++ 네이티브 바인딩을 해석하지 못해 에러를 뿜을 때가 많습니다. 이 경우 vite.config.ts에 아래 설정을 추가해 주시면 간단히 해결됩니다.

export default defineConfig({
  ssr: {
    external: ['duckdb'] // Vite가 DuckDB를 번들링하지 않고 순수 Node 모듈로 두게 함
  }
});

DuckDB 서버와 클라이언트에서 연동

📦 0. 패키지 설치

두 환경을 모두 지원하기 위해 각각의 라이브러리를 설치해야 합니다. 터미널에서 다음 명령어를 실행해 주세요.

# 서버 사이드(Node.js)용
npm install duckdb

# 클라이언트 사이드(브라우저 WASM)용
npm install @duckdb/duckdb-wasm

🖥️ 1. 서버 사이드 (SSR) 구현: Loader 연동

서버 환경에서는 대용량 파일 가공이나 데이터베이스 조회를 전담합니다.

여기서 가장 중요한 핵심은 싱글톤(Singleton) 패턴입니다. 사용자가 접속할 때마다 DB를 새로 띄우면 서버 메모리가 터지거나 속도가 급격히 느려집니다. 딱 하나의 인스턴스만 유지하도록 설정해야 합니다.

① 싱글톤 DB 연결 파일 생성 (app/lib/duckdb.server.ts)

파일명에 .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[]);
    });
  });
}

② 라우트에서 활용 (app/routes/sales.tsx)

import { useLoaderData } from "react-router";
import { queryAsync } from "~/lib/duckdb.server";

// 1. 서버 로더에서 무거운 CSV 데이터 가공
export async function loader() {
  const sql = `
    SELECT product_id, SUM(quantity) as total_sales 
    FROM 'server/data/orders.csv' 
    GROUP BY product_id 
    LIMIT 10
  `;
  const salesData = await queryAsync(sql);
  return { salesData };
}

// 2. 가공된 결과만 브라우저로 전달하여 가볍게 렌더링
export default function SalesPage() {
  const { salesData } = useLoaderData<typeof loader>();

  return (
    <div className="p-4">
      <h1 className="text-xl font-bold">서버 DuckDB 집계 데이터</h1>
      {/* 데이터 렌더링 코드... */}
    </div>
  );
}

🌐 2. 클라이언트 사이드 (WASM) 구현: React 훅 연동

클라이언트 환경에서는 사용자가 올린 CSV 파일 분석이나, 서버에서 가져온 로우(Raw) 데이터를 클라이언트 기기 성능을 이용해 오프라인으로 쪼갤 때 씁니다.

WASM은 초기 로딩이 무겁기 때문에, **리액트 커스텀 훅(Custom Hook)**으로 감싸서 전역 또는 필요한 컴포넌트에서 비동기로 관리하는 것이 정신건강에 이롭습니다.

① DuckDB WASM 커스텀 훅 생성 (app/hooks/useDuckDb.ts)

import { useState, useEffect } from "react";
import * as duckdb from "@duckdb/duckdb-wasm";

export function useDuckDb() {
  const [db, setDb] = useState<duckdb.AsyncDuckDB | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function init() {
      try {
        const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
        const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
        
        const worker = new Worker(bundle.mainWorker!);
        const asyncDb = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
        await asyncDb.instantiate(bundle.mainModule, bundle.pthreadWorker);
        
        setDb(asyncDb);
      } catch (error) {
        console.error("DuckDB WASM 초기화 실패:", error);
      } finally {
        setLoading(false);
      }
    }
    
    init();
  }, []);

  return { db, loading };
}

② 컴포넌트에서 활용 (app/routes/client-upload.tsx)

import { useState } from "react";
import { useDuckDb } from "~/hooks/useDuckDb";

export default function ClientUpload() {
  const { db, loading } = useDuckDb();
  const [results, setResults] = useState<any[]>([]);

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || !db) return;

    // 브라우저로 들어온 파일을 DuckDB WASM의 가상 파일 시스템에 등록
    await db.registerFileHandle('user_upload.csv', file, duckdb.DuckDBDataProtocol.BROWSER_FILEREADER, false);
    
    const conn = await db.connect();
    const res = await conn.query(`SELECT * FROM 'user_upload.csv' LIMIT 10`);
    
    setResults(res.toArray().map(r => r.toJSON()));
    await conn.close();
  };

  if (loading) return <div>브라우저 내 DuckDB 엔진 로딩 중...</div>;

  return (
    <div className="p-4">
      <h1 className="text-xl font-bold">클라이언트 DuckDB 분석</h1>
      <input type="file" accept=".csv" onChange={handleFileChange} />
      {/* 결과 렌더링... */}
    </div>
  );
}

🛠️ 3. 프로젝트 환경 설정 (가장 중요 ⭐⭐⭐)

서버 환경에서 C++ 네이티브 모듈인 duckdb를 사용하면 Vite 번들러가 이를 해석하지 못해 100% 에러가 발생합니다. vite.config.ts 파일을 열어 다음 설정을 반드시 추가해 주어야 양쪽 모두 정상 작동합니다.

// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
  ssr: {
    // ⚠️ 서버 전용 라이브러리인 duckdb를 Vite 번들링에서 제외합니다. (필수!)
    external: ['duckdb'], 
  },
  optimizeDeps: {
    // 클라이언트 WASM 모듈을 최적화 사전 로드 목록에 추가합니다.
    include: ['@duckdb/duckdb-wasm']
  }
});

Cloudflare Workers에서 DuckDB WASM 사용하기

🛑 과거에(혹은 일반적으로) 왜 어렵다고 했을까?

  • 스레드(Thread) 모델: 브라우저용 DuckDB는 멀티스레드나 브라우저의 Web Worker, SharedArrayBuffer 같은 기능에 의존하는 경우가 많습니다. Cloudflare Workers는 단일 스레드로 동작하는 가벼운 환경이라 이 부분이 충돌납니다.
  • 용량 제한: Cloudflare Workers 무료 플랜은 코드 크기 제한이 3MB입니다. 그런데 웬만한 DuckDB WASM 파일은 압축해도 3MB를 가볍게 넘깁니다.

🛠️ 하지만 지금은 가능합니다! (해결책)

오픈소스 생태계에서는 이미 이 한계를 뚫어낸 Cloudflare Workers 전용 DuckDB WASM 빌드들이 존재합니다 (대표적으로 Ducklings 같은 프로젝트가 있습니다).

브라우저 종속적인 기능을 걷어내고, Workers의 fetch API와 DuckDB의 C++ 코드를 매끄럽게 이어주는 특수 빌드를 사용하면 Workers 안에서도 DuckDB를 쌩쌩 돌릴 수 있습니다.

⚠️ 현실적으로 꼭 알아야 할 제약 사항

  1. 유료 플랜 필수: 최적화된 Workers용 DuckDB WASM 파일도 용량이 약 9.7MB 내외입니다. 따라서 Workers의 **유료 플랜(Paid Plan, 월 5$ / 코드 용량 10MB까지 허용)**을 사용하셔야만 배포가 가능합니다.
  2. 메모리 제한: Cloudflare Workers는 인스턴스당 메모리가 128MB로 제한됩니다. 따라서 수십 GB짜리 데이터를 메모리에 다 올리는 쿼리는 불가능하며, 똑똑하게 인덱싱된 파일이나 적절한 크기의 데이터를 다루셔야 합니다.
  3. 단일 스레드: Workers 환경의 한계로 멀티스레드 병렬 처리가 불가능해 브라우저나 서버만큼의 풀 파워를 내기는 어렵습니다.

Cloudflare Workers + DuckDB WASM + R2

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 같은 프로젝트가 있습니다).


⚠️ 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 패키지를 설치합니다.

npm install @ducklings/workers

💻 3. Workers용 DuckDB 구현 예시 (TypeScript)

Workers 내에서 DuckDB를 사용할 때 가장 중요한 점은, 매번 요청이 들어올 때마다 WASM을 새로 파싱하고 초기화하면 응답 속도가 심각하게 떨어진다는 것입니다. 전역 변수를 활용해 한 번 생성한 DB 인스턴스를 재사용(Reuse)해야 합니다.

// src/index.ts
import { init, DuckDB } from '@ducklings/workers';
import wasmModule from '@ducklings/workers/wasm';

// ⚠️ 전역 스코프에 변수를 선언하여 동일한 Isolates(인스턴스)로 
// 들어오는 요청들이 DB 인스턴스를 공유할 수 있게 합니다.
let db: DuckDB | null = null;

export default {
  async fetch(request: Request, env: any, ctx: any): Promise<Response> {
    try {
      // 1. DuckDB 인스턴스가 없다면 최초 1회 초기화
      if (!db) {
        db = await init(wasmModule);
      }
      
      // 2. 데이터베이스 커넥션 열기
      const conn = await db.connect();
      
      // 3. 쿼리 실행
      // Workers 전용 빌드에는 Parquet 및 httpfs 익스텐션이 내장되어 있습니다.
      // ⚠️ R2 버킷의 데이터를 쿼리할 때는 공개된 URL(또는 커스텀 도메인)을 사용합니다.
      const sql = `
        SELECT category, COUNT(*) as total
        FROM 'https://data.your-domain.com/sales_logs.parquet'
        GROUP BY category
        ORDER BY total DESC
        LIMIT 5;
      `;
      
      const queryResult = await conn.query(sql);
      
      // 4. 결과를 JSON으로 변환
      const results = queryResult.toArray().map(r => r.toJSON());
      
      // 5. 커넥션 종료 (Isolates가 유지되더라도 커넥션은 닫아주는 것이 안전합니다)
      await conn.close();
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' },
      });
      
    } catch (error: any) {
      return new Response(JSON.stringify({ error: error.message }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }
  },
};

🌐 4. Cloudflare R2 비공개 버킷에서 파일 읽기

만약 데이터를 인터넷 전체에 공개하고 싶지 않다면, 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_IDR2_SECRET_ACCESS_KEY와 같은 자격 증명은 절대 코드에 하드코딩하지 마시고, Wrangler의 wrangler secret put 명령어를 이용해 환경 변수로 주입해 주어야 안전합니다.


Cloudflare Workers + DuckDB WASM + R2 배포 가이드

Cloudflare Workers에서 DuckDB를 실행하려면 일반적인 번들링 방식과는 조금 다른 접근이 필요합니다. 특히 WASM 파일의 경로 처리와 유료 플랜 설정을 위한 wrangler.toml 구성이 핵심입니다.

배포까지 완벽하게 마칠 수 있도록 프로젝트 구성 및 배포 가이드를 정리해 드립니다.


🛠️ 1. 프로젝트 설정 (wrangler.toml)

Cloudflare Workers에 DuckDB WASM을 올리려면 유료 플랜(Standard/Paid) 설정과 함께, WASM 모듈을 에셋으로 인식하도록 설정해야 합니다.

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'으로 별도 관리 권장

🏗️ 2. Vite / Wrangler 번들링 최적화

Workers 환경에서 @ducklings/workers/wasm과 같은 바이너리 파일을 불러올 때, 번들러가 이를 텍스트가 아닌 WebAssembly 모듈로 정확히 인식하게 해야 합니다.

만약 Vite를 사용 중이라면 vite.config.ts에 다음 처리가 필요할 수 있습니다.

// vite.config.ts (Workers 환경용)
export default {
  build: {
    assetsInlineLimit: 0, // WASM 파일이 소스코드에 인라인화되지 않도록 방지
  },
  server: {
    fs: {
      allow: ['..'] // 프로젝트 상위 폴더의 WASM 참조 허용
    }
  }
};

🚀 3. 실전 배포 절차

코드가 준비되었다면 터미널에서 다음 순서로 명령어를 실행하여 배포를 완료합니다.

  1. 로그인 및 인증:
    npx wrangler login
  2. 보안 키 등록 (R2 사용 시):
    # 비밀번호와 같은 민감 정보는 환경 변수 대신 Secret으로 등록합니다.
    npx wrangler secret put R2_SECRET_ACCESS_KEY
  3. 배포 실행:
    npx wrangler deploy

💡 최종 아키텍처 요약

전체적인 데이터 흐름은 다음과 같습니다:

  1. 데이터 저장: 대용량 로그(CSV/JSON)나 분석용 Parquet 파일을 Cloudflare R2에 저장합니다.
  2. 데이터 조회: 사용자가 API 요청을 보내면 Cloudflare Worker가 깨어납니다.
  3. 엔진 로드: Worker 내부의 DuckDB WASM 인스턴스가 (이미 떠 있지 않다면) 초기화됩니다.
  4. 연산 처리: DuckDB가 R2에 있는 파일의 필요한 부분(HTTP Range Request)만 읽어와서 SQL 연산을 수행합니다.
  5. 응답 반환: 최종 집계된 결과(JSON)만 클라이언트로 전송합니다.

이 구조의 최대 장점은 **"서버 유지비가 거의 들지 않으면서(Serverless), 기가바이트 단위의 데이터를 단 몇 초 만에 SQL로 분석할 수 있다"**는 점입니다.


마지막 팁: 배포 후 쿼리 속도가 느리다면, 원본 데이터를 Parquet 형식으로 변환하여 R2에 저장했는지 꼭 확인하세요. DuckDB는 Parquet의 메타데이터를 읽어 필요한 컬럼만 쏙쏙 골라 가져오기 때문에, 전체 파일을 다운로드하지 않아 Workers 환경에서 성능이 극대화됩니다.

On this page

DuckDB1. DuckDB의 핵심 특징2. 설치 방법3. 실전 활용 가이드 (핵심 기능)① CSV 및 Parquet 파일 직접 쿼리하기② Python과 Pandas 완벽 연동💻 3. Workers용 DuckDB 구현 예시 (TypeScript)🌐 4. Cloudflare R2 비공개 버킷에서 파일 읽기2. DuckDB로 데이터프레임 직접 쿼리 (df라는 변수명을 그대로 사용)name0 Bob1 Charlie4. 유용한 CLI 명령어 (터미널 환경)CLI 환경1. DuckDB 실행 방식 (메모리 vs 디스크)2. CLI 출력 포맷 예쁘게 꾸미기 (필수 팁)3. 꼭 알아야 할 핵심 Dot(.) 명령어4. 외부 파일 처리하기 (가져오기 & 내보내기)① 파일의 쿼리 결과를 다른 파일로 바로 내보내기② 쿼리 결과를 텍스트 파일로 저장하기5. CLI 쉘 스크립트(Bash 등)와 연동하기DuckDB CLI 환경에서 CSV, JSON, 그리고 텍스트(로그) 파일을 다루는 구체적인 방법📊 1. CSV 파일 다루기① 기본 조회 및 자동 인식② 수동 설정으로 정밀하게 읽기 (read_csv 함수)③ 여러 CSV 파일 한 번에 묶어서 읽기 (와일드카드)🌐 2. JSON 파일 다루기① 기본 조회② 중첩된(Nested) JSON 데이터 추출하기③ JSON 배열 풀기 (Unnest)📝 3. 텍스트 및 로그(Log) 파일 다루기전략 A: 한 줄씩 통째로 읽어서 정규식(Regex)으로 파싱하기전략 B: 구분자가 있는 로그 (공백, 파이프 등)💡 CLI 환경 꿀팁: 쉘(Bash) 명령어로 파이프라인 만들기React Router v7과 DuckDB 조합하기📊 1. SSR(서버) vs WASM(클라이언트) 선택 가이드🌐 2. 서버 사이드 (SSR Mode) 활용법① 패키지 설치② 라우트 파일 작성 (app/routes/analytics.tsx)💻 3. 클라이언트 사이드 (WASM / SPA Mode) 활용법① 패키지 설치② 컴포넌트 작성 예시🛠️ v7 환경에서 자주 겪는 트러블슈팅 꿀팁DuckDB 서버와 클라이언트에서 연동📦 0. 패키지 설치🖥️ 1. 서버 사이드 (SSR) 구현: Loader 연동① 싱글톤 DB 연결 파일 생성 (app/lib/duckdb.server.ts)② 라우트에서 활용 (app/routes/sales.tsx)🌐 2. 클라이언트 사이드 (WASM) 구현: React 훅 연동① DuckDB WASM 커스텀 훅 생성 (app/hooks/useDuckDb.ts)② 컴포넌트에서 활용 (app/routes/client-upload.tsx)🛠️ 3. 프로젝트 환경 설정 (가장 중요 ⭐⭐⭐)Cloudflare Workers에서 DuckDB WASM 사용하기🛑 과거에(혹은 일반적으로) 왜 어렵다고 했을까?🛠️ 하지만 지금은 가능합니다! (해결책)⚠️ 현실적으로 꼭 알아야 할 제약 사항Cloudflare Workers + DuckDB WASM + R2⚠️ 1. 구현 전 필수 체크 리스트📦 2. 패키지 설치💻 3. Workers용 DuckDB 구현 예시 (TypeScript)🌐 4. Cloudflare R2 비공개 버킷에서 파일 읽기Cloudflare Workers + DuckDB WASM + R2 배포 가이드🛠️ 1. 프로젝트 설정 (wrangler.toml)🏗️ 2. Vite / Wrangler 번들링 최적화🚀 3. 실전 배포 절차💡 최종 아키텍처 요약