본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 19:35

SQL로 돈과 마감일에 관한 질문에 답하는 개인용 Coral 에이전트, 'Ask Your Life'를 만들었습니다

요약

사용자의 이메일, 캘린더, 결제 내역 등 분산된 데이터를 SQL로 조인하여 개인의 행정적 리스크를 탐지하는 'Life Risk Radar' 에이전트 개발기입니다. Coral SQL과 Claude를 결합하여 질문을 실행 가능한 데이터 쿼리로 변환하고 구체적인 대응 초안까지 제공하는 아키텍처를 다룹니다.

핵심 포인트

  • 분산된 개인 데이터를 SQL로 조인하여 리스크 탐지
  • Coral SQL과 Claude를 활용한 에이전트 루프 구현
  • 질문 기반의 'Ask your life'와 스캔 기반의 'Scan everything' 모드 제공
  • 단순 정보 제공을 넘어 실행 가능한 초안(drafted action) 생성

"이번 주에 저에게 돈, 시간 또는 접근 권한의 손실을 초래할 수 있는 개인 행정 리스크는 다음과 같습니다 — 증빙 자료 포함." 🪸

이 한 문장이 바로 WeMakeDevs의 Pirates of the Coral-bean 해커톤(Personal Agent 트랙) 출전작인 Life Risk Radar의 핵심 피치(pitch)입니다. 이것은 여러분이 이메일 수신함, 캘린더(Calendar), 그리고 돈에 대해 평이한 영어로 질문할 수 있는 개인용 에이전트이며, 실제 교차 소스 SQL 조인 (cross-source SQL join), 모든 수치 뒤에 숨겨진 증거, 그리고 여러분이 보낼 수 있는 **작성된 실행 초안 (drafted action)**과 함께 답변을 제공합니다.

이 포스트는 전체 빌드 스토리입니다: 문제점, 아키텍처(architecture), Coral이 어떻게 "여러분의 삶"을 쿼리 가능한 데이터베이스(queryable database)로 바꾸는지, Claude와 Coral SQL을 결합하는 에이전트 루프(agent loop), 안전 모델(safety model), 그리고 그 과정에서 발생한 문제들(주석이 아니었던 --, Cloudflare 403 오류, 그리고 제 SQL이 실제로 아무것도 조인하고 있지 않다는 것을 깨달은 순간)을 다룹니다.

문제점: 행정 업무를 통해 새어나가는 여러분의 돈

돈, 시간, 그리고 접근 권한은 일상적인 행정 업무의 틈새를 통해 거의 항상 조용히 새어나갑니다:

  • 내일 연간 96달러로 자동 갱신되는 무료 체험 💸
  • 조용히 닫히고 있는 Amazon 반품 가능 기간 🧾
  • 전혀 눈치채지 못한 중복 구독 결제 🪞
  • 280달러의 수수료를 확정 짓는 호텔 무료 취소 마감일
  • 서류 누락으로 인해 실패하게 될 여권 예약 📄
  • 계좌를 동결시킬 수 있는 은행 KYC(Know Your Customer) 마감일 🪪

중요한 점은 이것입니다: 이 모든 것에 대한 증거는 이미 여러분의 계정에 있습니다. 갱신 이메일은 Gmail에 있습니다. 결제 내역은 카드 명세서에 있습니다. 예약은 캘린더(Calendar)에 있습니다. 누락된 파일은 문서 폴더에 있습니다.

문제는 데이터의 부족이 아니었습니다. 문제는 아무도 그것을 조인(join)하지 않는다는 것입니다. 여러분의 이메일은 거래 내역을 알지 못합니다. 여러분의 캘린더는 어떤 문서가 누락되었는지 알지 못합니다. 각 소스(source)는 하나의 섬이며, 리스크는 그 섬들 _사이_의 간극에 존재합니다.

그 간극이 바로 Coral이 메우기 위해 만들어진 지점입니다.

내가 만든 것

Life Risk Radar에는 두 가지 모드가 있으며, 그 순서가 중요합니다.

1. Ask your life (주인공). 질문을 입력하는 커맨드 바(command bar)입니다:

  • "이번 주에 내 돈을 낭비하게 만드는 것이 무엇인가?"
  • "어딘가에서 이중 청구가 되고 있는가?"
  • "어떤 누락된 문서가 마감일을 가장 많이 방해하고 있는가?"
  • "갱신될 예정인 구독 서비스나 무료 체험 기간은 무엇인가?"

에이전트는 사용자의 질문을 Coral SQL로 변환하고, 실제 교차 소스 조인 (cross-source join)을 실행한 뒤, 한 줄 요약 판결, 행당 결과 카드, 실행된 정확한 SQL, 그리고 즉시 보낼 수 있도록 작성된 실행 초안과 함께 답변을 제공합니다.

2. Scan everything (백업). 질문하고 싶지 않으신가요? 버튼 하나로 모든 소스를 훑고, 리스크의 순위를 매긴 뒤, 단계별 "이 리스크를 해결하기" 액션 플랜을 열어볼 수 있는 보드 형태로 나열합니다.

전체적인 디자인은 라이트 모드(light-mode)이며, 편집자 스타일(editorial)을 지향하며, 의도적으로 대시보드스럽지 않게(un-dashboardy) 만들었습니다. 왜냐하면 주인공은 차트가 아니라, 답변과 그 뒤에 숨겨진 쿼리(query)이기 때문입니다.

전환점: 대시보드에서 에이전트로

이 프로젝트가 어떻게 시작되었는지 솔직하게 말씀드리겠습니다. 전환점이 이 이야기에서 가장 유용한 부분이기 때문입니다.

저의 첫 번째 버전은 깔끔한 리스크 **대시보드 (dashboard)**였습니다. "Scan"을 클릭하면 순위가 매겨진 카드들이 나타났죠. 완성된 것처럼 보였습니다. 그러다 이를 구동하는 SQL을 열어보고 저는 스스로를 발견했습니다:

WITH gmail AS (SELECT ... FROM life_files.gmail_messages),
     deadlines AS (SELECT ... FROM life_files.manual_deadlines),
     documents AS (SELECT ... FROM life_files.documents),
...

다섯 가지 소스에 대해 CTE (Common Table Expressions)를 _선언_만 해두고... 정작 그중 어느 것도 조인 (join)하지 않았습니다. "교차 소스 증거"는 사실 나중에 TypeScript에서 퍼지 문자열 매칭 (fuzzy string matcher)을 통해 이어 붙여지고 있었습니다. 저는 Coral을 조인 엔진 (join engine)이 아닌, 그저 화려한 파일 리더 (file reader)로 사용하고 있었던 것입니다.

그것은 거꾸로 된 방식입니다. Coral이 제공하는 가장 가치 있는 단 한 가지는 이메일, 캘린더, 파일, API와 같이 **완전히 다른 소스들을 마치 하나의 데이터베이스인 것처럼 조인하는 SQL 인터페이스 (SQL interface)**입니다. 만약 제 SQL이 조인을 수행하지 않고 있다면, 저는 Coral을 제대로 사용하고 있는 것이 아니었습니다.

그래서 저는 방향을 전환했습니다. 데이터나 UI 작업을 바꾼 것이 아니라, 바로 _가설 (thesis)_을 바꾼 것입니다. "Coral에서 데이터를 읽어오는 대시보드"에서 **"전체 업무가 Coral에 올바른 교차 소스 질문을 던지는 에이전트"**로 말이죠. 이러한 재정의가 괜찮은 프로젝트를 관점이 있는 프로젝트로 탈바꿈시켰습니다.

Coral의 역할

아직 사용해 보지 않으셨다면: Coral은 작은 스펙 파일로 설명된 "소스 (sources)" — API, 파일, 캘린더, 데이터베이스 등 — 를 가리키는 로컬 우선 (local-first) SQL 엔진이며, 일반 SQL로 이들을 쿼리(및 조인 (join))할 수 있게 해줍니다. 또한 에이전트가 이를 도구로 직접 사용할 수 있도록 MCP 서버 (coral mcp-stdio)도 함께 제공합니다.

Life Risk Radar는 추가적인 파이프라인 없이 두 개의 소스를 사용합니다:

  • life_files — 다섯 개의 테이블(transactions, documents, manual_deadlines, calendar_events, gmail_messages)을 노출하는 JSONL 기반 소스입니다. 이는 재현 가능한 데모 데이터이므로, 개인 편지함에 접근하지 않고도 프로젝트를 처음부터 끝까지 실행할 수 있습니다.
  • gmail — OAuth 토큰을 사용하여 실제 Gmail API에 접속하는 HTTP 소스 스펙이며, 나중에 실사용할 수 있도록 message_searchmessage_details를 노출합니다.

소스를 등록하는 방법은 두 개의 명령어로 충분합니다:

coral source add --file sources/life_files/manifest.yaml
coral source test life_files

...이제 다섯 개의 서로 다른 "섬"들이 조인 가능한 하나의 스키마가 되었습니다.

아키텍처 (The architecture)

자유 형식 질문(free-text question)에 대한 요청 흐름은 다음과 같습니다:

질문을 입력합니다
        │
        ▼
...

이것은 진정한 2단계 에이전트 루프입니다:

  1. 질문 → SQL. Claude에게 스키마가 제공되고 하나의 Coral 쿼리를 작성합니다.
  2. 검증 (Validate). 생성된 SQL은 무언가에 접근하기 전에 읽기 전용 허용 목록 (allowlist)을 통과해야 합니다.
  3. 실행 (Run). Coral이 조인을 실행하고 근거가 첨부된 행 (rows)을 반환합니다.
  4. 행 → 답변. Claude가 실제 결과를 읽고 헤드라인과 초안 작업 (drafted action)을 작성합니다.

그 후 UI에는 실행된 SQL이 표시됩니다. 이러한 투명성은 디자인의 핵심 요소입니다. 쿼리와 결합된 증거(joined evidence)를 직접 볼 수 있을 때, 에이전트는 더 이상 마법처럼 느껴지지 않고 검사 가능한 (inspectable) 존재로 느껴지기 시작합니다. 이는 사용자의 돈을 다루는 도구로서 매우 중요한 부분입니다.

기술 스택: Next.js + TypeScript, Chakra UI (가벼운 에디토리얼 테마 — Fraunces + Hanken Grotesk), SQL/조인(joins)을 위한 Coral, 그리고 NL(자연어)→SQL 변환 및 요약을 위한 **Claude (Opus 4.7)**를 사용했으며, 스키마 시스템 프롬프트에 프롬프트 캐싱(prompt caching)을 적용했습니다.

내가 자랑스럽게 생각하는 쿼리: 단일 장애점 (single point of failure)

대시보드로는 결코 제대로 답할 수 없는 질문이 하나 있습니다: "어떤 누락된 문서가 가장 많은 마감일을 가로막고 있는가?"

SELECT
  doc.name AS missing_document,
  COUNT(DISTINCT dl.id) AS deadlines_blocked,
...

답변:

address_proof.pdf가 2개의 마감일을 가로막고 있습니다 — 단일 장애점 (single point of failure)입니다.
(여권 예약과 은행 KYC 모두 이 문서가 필요합니다.)

단 하나의 누락된 파일이 두 개의 놓친 마감일로 이어지는 상황이 단 한 줄의 행(row)으로 드러납니다. 이것이 바로 조인(join)이 생성해내는 통찰이며, 단순한 카드 리스트는 결코 제공할 수 없는 가치입니다.

다른 두 가지 주요 질문 역시 똑같이 실질적인 조인으로 연결됩니다:

질문교차 소스 조인 (Cross-source join)연결되는 정보
"중복 결제가 되었나요?"transactionsgmail_messages중복 결제 내역 옆에 영수증 이메일 표시
"이번 주에 돈이 어디로 새고 있나요?"manual_deadlinesgmail_messagescalendar_events청구 이메일 캘린더 알림

예를 들어, 중복 결제 쿼리는 카드 거래 내역을 이를 설명하는 영수증 이메일과 조인합니다:

SELECT t.merchant, COUNT(*) AS charge_count, SUM(t.amount) AS total_amount,
       MAX(g.subject) AS receipt_evidence
FROM life_files.transactions t
...

→ _"Adobe에서 2회 결제됨 — 검토 비용 $39.98"_라는 결과와 함께, 조인을 통해 일치하는 "Adobe payment receipt - $19.99" 이메일이 함께 불러와집니다. 세 개의 소스가 하나의 행으로 합쳐지며, 모든 숫자는 그 출처를 추적할 수 있습니다.

에이전트 루프, 코드로 보기

질문(Question) → SQL은 스키마에 기반하며 캐싱된 단일 Claude 호출입니다:

const message = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 700,
...

그 다음 Coral이 이를 실행하며, 두 번째 Claude 호출이 실제(actual) 행들을 읽고 인간을 위한 답변을 작성합니다:

const rows = await runCoral(sql);                 // 실제 교차 소스 조인 (cross-source join)
const summary = await summarizeWithClaude(question, columns, rows);
// → { headline: "…", draft: { subject, body } }

이것이 이 시스템을 단순한 검색창이 아닌 에이전트(Agent)로 만드는 핵심입니다. 즉, 질문뿐만 아니라 결과에 대해서도 추론(Reasoning)을 수행합니다.

안전하게 유지하기 — 그리고 데모에서 깨지지 않게 만들기

두 가지 엔지니어링 제약 사항이 구축 과정을 결정했습니다.

1. 생성된 SQL은 샌드박스(Sandboxed) 처리됩니다. Claude가 작성한 모든 내용은 Coral에 도달하기 전에 검증됩니다. 단일 문장이어야 하며, SELECT/WITH만 허용되고, 허용된 스키마만 사용 가능하며, DDL/DML은 금지됩니다:

if (trimmed.includes(";"))        return reject("단일 문장만 허용됩니다.");
if (!/^(select|with)\b/i.test(trimmed)) return reject("SELECT/WITH만 허용됩니다.");
if (FORBIDDEN.test(trimmed))      return reject("Write/DDL 키워드는 허용되지 않습니다.");
...

DROP TABLE, UPDATE, 다중 문장 주입(multi-statement injection), 그리고 secrets.users 참조를 대상으로 단위 테스트(Unit test)를 수행한 결과 모두 거부되었으며, life_files에 대한 정당한 SELECT/WITH는 허용되었습니다.

2. 데모는 절대 깨져서는 안 됩니다. 세 단계의 우아한 성능 저하(Graceful degradation) 계층을 두었습니다:

  • 네 가지 헤드라인 질문은 **수동으로 검증된 조인 쿼리(hand-vetted join queries)**와 함께 제공되므로, API 키가 전혀 없어도 앱이 작동합니다. Claude는 자유 형식의 질문(free-text questions)에만 동력을 공급합니다.
  • Coral CLI가 없는 경우(예: 서버리스 배포), TypeScript를 사용하여 로컬 JSONL에서 동일한 답변을 계산하는 방식으로 대체(Fallback)됩니다.
  • 데모는 시드된 재현 가능한 데이터(seeded, reproducible data) — 전용 데모 Google 계정 및 로컬 샘플 파일 — 위에서 실행되므로, 누구나 개인 편지함에 손을 대지 않고도 엔드 투 엔드(end-to-end)로 실행할 수 있습니다.

이것이 "새벽 2시에 내 컴퓨터에서 돌아가는 것"과 "무대 위에서 돌아가는 것"의 차이입니다.

문제가 되었던 것들 (그리고 이를 통해 배운 점)

완벽한 빌드는 없습니다. 솔직한 기록을 남깁니다:

  • SQL 조인(Join)이 작동하지 않았습니다. 위에서 다루었듯이 — 가장 중요한 수정 사항은 코드가 아니라 가설이었습니다. TypeScript의 문자열 매칭 대신 실제 조인(documents ⋈ deadlines, transactions ⋈ gmail)을 사용했습니다.
  • 주석이 아니었던 -- 기호. 제 쿼리 파일은 -- "이번 주에 내 돈을 쓰게 만드는 것이 무엇인가?"로 시작합니다. 이를 coral sql "<query>"로 전달하면 CLI가 맨 앞의 --를 플래그(flag)로 인식하여 오류를 발생시켰습니다. 해결책은 옵션 종료 구분자(end-of-options separator)를 사용하는 것이었습니다 — coral sql --format json -- "$(cat query.sql)".
  • 인증 문제가 아니었던 403 오류. 바로 이 포스트를 게시하는 과정에서 HTTP 403 오류가 발생했습니다. API 키는 유효했습니다. Dev.to는 Cloudflare 뒤에 있으며, Cloudflare는 기본 Python-urllib User-Agent를 차단합니다. 한 줄의 User-Agent 헤더 추가로 해결되었습니다. 403이 반드시 "잘못된 자격 증명"을 의미하지는 않는다는 좋은 교훈을 얻었습니다.
  • 절반만 설치된 node_modules. 설치가 중단되면서 빈 패키지 폴더들이 남았고, 이는 오래된 증분 타입 체크(incremental typecheck)는 통과했지만 클린 빌드(clean build)에서는 실패했습니다. 실제 해결책은 깨끗한 npm ci 실행이었으며, 캐시된 초록색 체크 표시를 절대 믿지 말아야 한다는 교훈을 얻었습니다.

다음 단계: 실제 계정 연결하기

라이브 계정을 읽기 위한 배관 작업(plumbing)은 이미 존재합니다 — gmail HTTP 소스 명세(spec)와 읽기 전용 OAuth가 그것입니다. 남은 작업은 진정으로 어려운 부분입니다:

  1. 일반화된 추출 (Generalized extraction). 데모의 리스크 규칙은 "Notion", "Adobe", "passport"를 알고 있습니다. 실제 편지함은 임의의(arbitrary) 이메일을 금액, 날짜, 의도로 변환해야 합니다. 이는 동일한 Coral 테이블에 데이터를 공급하는 LLM 추출 단계에 매우 적합합니다.
  2. 라이브 트랜잭션 피드 (A live transactions feed) (명세서 가져오기 또는 애그리게이터)를 통해 실제 지출에 대해 중복 탐지가 실행되도록 합니다.
  3. Gmail 명세를 미러링하는 라이브 캘린더(Calendar) 소스 명세.

하지만 핵심적인 가설은 이미 증명되었습니다: 당신의 삶은 쿼리 가능하며, 통찰(insight)은 조인(join)이 일어나는 곳에 존재합니다.

시도해보기 / 요약

만약 여러분이 Coral을 기반으로 구축한다면, 제가 전하고 싶은 교훈은 간단합니다. 단순히 소스(source)를 읽기만 하지 말고, 그것들을 조인(join)하세요. 레코드(record)의 목록은 번거로운 작업일 뿐이지만, 조인된 행(row)은 통찰(insight)이 됩니다 ("이 누락된 파일 하나가 두 개의 마감일을 가로막고 있습니다"). 그리고 생성된 SQL을 화면에 표시한다면, 여러분의 에이전트는 신뢰를 구걸하는 대신 신뢰를 얻게 될 것입니다.

Life Risk Radar는 WeMakeDevs의 Pirates of the Coral-bean 해커톤(Personal Agent 트랙)을 위해 제작되었습니다. 자연어 질문이 입력되면, 실제 교차 소스 SQL(cross-source SQL)이 출력되며, 매번 증거와 초안 작업(drafted action)을 함께 제공합니다. 🪸

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0