웨어러블 데이터에 빠지지 마세요: DuckDB와 Apache Arrow로 통합 건강 데이터 레이크 구축
Source: Dev.to
Quantified Self 운동을 좋아한다면 그 고충을 잘 알 겁니다: Oura Ring은 수면을 추적하고, Whoop은 회복을 분석하며, Garmin은 달리기 기록을 남깁니다. 하지만 “내 훈련 부하가 REM 수면에 어떤 영향을 미치는가?” 같은 간단한 질문에 답하려고 하면, CSV 내보내기와 호환되지 않는 JSON 스키마의 악몽에 갇히게 됩니다. 이질적인 웨어러블 데이터를 위한 ETL 파이프라인을 관리하는 일은 데이터 엔지니어의 인내심을 시험하는 궁극적인 도전입니다.
이 가이드에서는 이러한 데이터 사일로를 해체할 것입니다. DuckDB, Apache Arrow, 그리고 TypeScript를 사용해 고성능, 로컬‑우선 데이터 레이크를 구축합니다. 최종적으로는 모든 기기 데이터를 밀리초 단위로 복잡한 OLAP 쿼리를 실행할 수 있는 통합 스토어를 갖게 됩니다. 보다 프로덕션에 적합한 패턴과 고급 건강 데이터 동기화에 관심이 있다면, WellAlly Blog에서 심층 다이브를 확인해 보시길 강력히 추천합니다.
아키텍처: 혼돈에서 통찰로
헬스 데이터 엔지니어링에서 가장 큰 과제는 데이터 정규화입니다. Oura는 심박 변동성(HRV)을 평균값으로 보고할 수 있지만, Whoop은 원시 시계열 데이터를 제공합니다. 우리의 파이프라인은 이를 통합된 Parquet 기반 스토리지로 평탄화하는 번역 레이어 역할을 합니다.
데이터 흐름 다이어그램
graph TD
A[Oura API / JSON] -->|Normalize| D[Unified Schema]
B[Whoop API / JSON] -->|Normalize| D
C[Garmin Fit Files] -->|Extract| D
D -->|Arrow IPC| E{DuckDB‑Wasm}
E -->|Persistent Storage| F[(OPFS / Parquet)]
G[Streamlit Dashboard] -->|SQL Query| E
E -->|Visuals| G
필수 조건
따라 하려면 다음이 필요합니다:
- Node.js/TypeScript – 정규화 로직을 위해.
- DuckDB‑Wasm – 브라우저 내/로컬 데이터베이스 엔진을 위해.
- Apache Arrow – 제로 카피 메모리 전송을 위해.
- Streamlit (Python) – 최종 분석 UI를 위해.
Step 1: 통합 건강 스키마 정의
먼저, “골든 레코드” 형식이 필요합니다. 우리는 TypeScript를 사용하여 모든 제공자가 매핑해야 하는 엄격한 인터페이스를 정의합니다.
// types/health.ts
export interface UnifiedActivity {
timestamp: Date;
source_device: 'Oura' | 'Whoop' | 'Garmin';
metric_type: 'HRV' | 'RHR' | 'Steps' | 'Calories';
value: number;
unit: string;
metadata: Record;
}
2단계: Apache Arrow를 사용한 정규화
원시 JSON을 데이터베이스에 직접 넣는 대신, Apache Arrow 버퍼로 변환합니다. 이렇게 하면 타입 안전성을 보장하고 DuckDB에 매우 빠르게 데이터를 삽입할 수 있습니다.
import { tableFromArrays, Table } from 'apache-arrow';
export function normalizeOuraData(rawData: any[]): Table {
const timestamps = rawData.map(d => new Date(d.timestamp).getTime());
const hrvValues = rawData.map(d => d.hrv_average);
// Create an Arrow Table
return tableFromArrays({
timestamp: new Int64Array(timestamps),
source_device: Array(rawData.length).fill('Oura'),
metric_type: Array(rawData.length).fill('HRV'),
value: new Float64Array(hrvValues),
unit: Array(rawData.length).fill('ms')
});
}
단계 3: DuckDB로 로컬 데이터 레이크 구축
이제 마법을 부릴 차례입니다. 우리는 DuckDB‑Wasm을 사용해 이 Arrow 테이블들을 ingest합니다. DuckDB는 분석 쿼리를 위해 설계된 컬럼형 데이터베이스로, 다년간의 건강 추세를 분석하기에 최적입니다.
import * as duckdb from '@duckdb/duckdb-wasm';
async function ingestToDuckDB(arrowTable: Table) {
const db = new duckdb.AsyncDuckDB(worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const conn = await db.connect();
// Register the Arrow table as a virtual view
await conn.insertArrowTable(arrowTable, { name: 'staging_data' });
// Create or Append to the persistent health_store
await conn.query(`
CREATE TABLE IF NOT EXISTS health_store AS
SELECT * FROM staging_data WHERE 1=0;
INSERT INTO health_store SELECT * FROM staging_data;
`);
console.log("Data normalized and ingested! 🚀");
}
Step 4: “공식적인” 확장 방법
로컬 ETL 도구를 만드는 것은 개인 사용에 좋지만, 수천 명의 사용자를 위한 건강 데이터 파이프라인을 확장하려면 OAuth 토큰 갱신, 속도 제한, 웹훅 리스너 등을 처리해야 합니다. 프로덕션 건강 앱을 구축하고 있다면 WellAlly Blog 에서 논의된 아키텍처 패턴을 살펴보세요. 여기서는 높은 동시성 데이터 수집 및 HIPAA 준수 스토리지 전략을 다루며, 단순한 DuckDB 인스턴스를 넘어서는 내용을 제공합니다.
Step 5: Streamlit으로 시각화하기
마지막으로, DuckDB 스토어를 Streamlit 대시보드에 감싸서 실제로 데이터를 볼 수 있게 합니다.
import streamlit as st
import duckdb
st.title("Unified Health Intelligence 🥑")
# Connect to the DuckDB file generated by our ETL
con = duckdb.connect(database='health_lake.db')
# Query correlation between Sleep Quality and Resting Heart Rate
df = con.execute("""
SELECT
CAST(timestamp AS DATE) AS date,
AVG(value) FILTER (WHERE metric_type = 'HRV') AS avg_hrv,
AVG(value) FILTER (WHERE metric_type = 'RHR') AS avg_rhr
FROM health_store
GROUP BY 1
ORDER BY 1 DESC
""").df()
st.line_chart(df, x='date', y=['avg_hrv', 'avg_rhr'])
결론
이제 다음과 같은 완전한 로컬‑우선 파이프라인을 갖추게 되었습니다:
- 이기종 웨어러블 데이터를 UnifiedActivity 스키마로 정규화합니다.
- Apache Arrow를 사용해 데이터를 효율적으로 전송합니다.
- 성능이 뛰어난 컬럼형 DuckDB‑Wasm 레이크에 저장합니다.
- Streamlit 대시보드를 통해 인사이트를 제공합니다.
개인 실험부터 프로덕션 수준의 헬스 플랫폼까지, 이 빌딩 블록들은 데이터를 여러분이 직접 제어하면서 빠르게 반복할 수 있는 유연성을 제공합니다. 즐거운 해킹 되세요! 🚀
데이터에 힘을 실어 주세요
“Quantified Self”는 독점 대시보드에 Quantified Slave(정량화된 노예)로 전락한다는 의미가 아닙니다. DuckDB와 Apache Arrow를 활용하여 다음과 같은 파이프라인을 구축했습니다:
- Fast: 컬럼형 저장소 덕분에 5년 치 기록을 밀리초 단위로 로드합니다.
- Private: 데이터가 로컬 환경에 그대로 유지됩니다.
- Flexible: 새로운 기기를 추가하는 것은 새로운 정규화 함수를 작성하는 것만큼 간단합니다.
다음에 무엇을 추적하고 싶나요? Apple Health나 Fitbit 데이터를 유사한 스택에 통합해 본 경험이 있다면 아래에 댓글을 남겨 주세요!
헬스 테크와 데이터 엔지니어링에 대한 더 고급 튜토리얼은 wellally.tech/blog에서 확인하세요. 🚀
