.md 파일이고, 검색을 빠르게 하는 SQLite는 "언제든 지웠다 다시 만들 수 있는 사본(투영본)"이라는 것. 이 문서는 소스를 직접 클론해 "이 저장소에서 무엇을 배울 수 있는가"를 파고든다.
(저장소: team-reflect/reflect-open · Tauri 2 + Rust + React 19 · MIT © 2026 Reflect App, LLC · 앱 v0.7.0-beta.8 · 최신 커밋 2026-07-17 a30a8aa · TrendShift Daily #17)
team-reflect/reflect-open(제품명 Reflect V2)은 유료 노트 앱 Reflect(reflect.app)를 오픈소스로 다시 만든 재작성판이다. 저장소의 AGENTS.md가 못 박는다 — "이 저장소는 Reflect V2이며, 원래의 Reflect 코드베이스를 오프라인 우선·마크다운 기반·오픈소스로 다시 쓴 것이다." 원래 Reflect는 데일리 노트와 백링크로 생각을 잇는 그래프형 노트 앱으로 유명했는데, 그 핵심 아이디어를 "열린 파일 위에서 돌아가게" 옮겨 놓은 게 이 저장소다.
가장 큰 오해를 먼저 풀자. 이름만 보면 흔한 웹 앱 같지만, 실제 정체는 Tauri 2로 만든 macOS·iOS 네이티브 앱이다. 겉껍데기(창·파일 접근·동기화·암호 저장)는 Rust가 맡고, 화면(에디터·목록·설정)은 그 안에 얹힌 React 19 웹뷰가 그린다. 그리고 노트의 원본은 데이터베이스가 아니라 평범한 마크다운 파일이다 — daily/2026-07-17.md, notes/무엇무엇.md처럼. 이걸 빠르게 검색·백링크하려고 SQLite 색인을 만들지만, 그 SQLite는 지워도 파일에서 다시 만들어지는 사본일 뿐이다.
.md 파일로 둬서, 앱이 사라져도 노트는 그대로 남는다..md 파일들. 무엇이 옳은지는 오직 이 파일이 결정한다. ② 투영본 = <그래프>/.reflect/index.sqlite. 파일들을 빠르게 뒤지려고 만든 색인 사본이다. 투영본은 언제든 지우고 파일에서 다시 빌드할 수 있다(단 하나의 예외: AI 대화 기록 chat_* 테이블만은 파일에서 못 만들어 내니 보존한다). "원본과 캐시를 분명히 갈라 놓는" 이 구조가 앱 전체 설계를 관통한다.Notion 같은 앱은 은행 금고와 같다 — 내 노트가 회사 서버에 있고, 나는 열쇠(계정)로 잠깐 열어 볼 뿐이다. 회사가 문을 닫으면 금고째 사라진다. reflect-open은 노트를 다시 내 서랍(디스크의 .md 파일)으로 가져온다. 앱이 없어져도 서랍 속 종이는 그대로다.
그런데 종이 노트의 약점은 "검색이 느리다"는 것. 그래서 이 서재엔 사서 로봇이 하나 있다. 서랍 내용이 바뀔 때마다 로봇(Rust 색인기)이 색인 카드(SQLite)를 자동으로 다시 만든다. 색인 카드를 통째로 태워 버려도, 로봇이 서랍을 다시 훑어 카드를 복원한다 — 원본은 언제나 서랍 속 종이다.
reflect-open이 트렌딩에 오른 이유는 단순한 "또 하나의 노트 앱"이어서가 아니다. 돈 받고 팔던 완성형 상용 앱을 통째로 MIT로 열었고, 그 과정에서 요즘 로컬 우선 앱들이 당연시하던 CRDT를 일부러 버리고 다른 길을 택했기 때문이다. 노트 앱을 만들거나 로컬 우선 동기화를 고민해 본 개발자에게 이 저장소는 "실제로 출시된 제품의 내부"를 통째로 뜯어볼 수 있는 흔치 않은 표본이다.
| 포인트 | 내용 |
|---|---|
| 정체 | 상용 Reflect의 로컬 우선·오픈소스 재작성판(V2). 데스크톱(mac)·모바일(iOS) 앱, 크롬 캡처 확장, reflect CLI까지 제품 전체를 열었다. 조각이 아니다. |
| 차별점 ① | 노트 = 마크다운 파일. 앱 전용 DB가 아니라 .md가 원본이라, Obsidian처럼 파일을 직접 만지거나 git으로 버전 관리할 수 있다. |
| 차별점 ② | CRDT를 버렸다. V1은 Yjs(CRDT)로 동기화했지만, V2는 파일 단위 결정론적 3-way 병합으로 충돌을 푼다. "로컬 우선 = 무조건 CRDT"라는 통념에 대한 반례. |
| 차별점 ③ | AI는 BYOK(Bring Your Own Key). Reflect 서버를 거치지 않고 내 OpenAI·Anthropic·Google 키로 직접 호출. 임베딩은 아예 기기 안에서(온디바이스) 돈다. |
| 제작자 | Reflect App, LLC — 실제 상용 제품을 운영하는 회사가 직접 재작성판을 오픈소스로 냈다. |
보통 상용 서비스가 오픈소스를 낼 때는 SDK나 일부 모듈만 공개한다. reflect-open은 다르다. macOS 앱 + iOS 앱(같은 코드베이스) + 크롬 확장 + 네이티브 메시징 호스트 + CLI까지, 제품을 이루는 거의 전부가 들어 있다. 게다가 기존 코드를 그대로 던진 게 아니라 철학을 바꿔 다시 썼다 — 파이어베이스(클라우드 DB) 기반이던 것을 "파일이 원본"으로, Electron/Next.js를 Tauri로, Yjs를 결정론적 병합으로. "제품을 리라이트하며 아키텍처를 갈아엎는" 실제 사례를 통째로 관찰할 수 있다.
2020년대 로컬 우선 앱의 정석은 Yjs·Automerge 같은 CRDT(충돌 없이 자동 병합되는 자료구조)다. 그런데 reflect-open V2는 이걸 의도적으로 걷어냈다. 대신 conflict/ladder.rs의 결정론적 충돌 해소 사다리로, 같은 충돌은 어느 기기에서 풀든 똑같은 결과 바이트가 나오게 만든다. 그래서 두 기기가 같은 충돌을 동시에 풀어도 결과가 수렴한다(핑퐁이 안 생긴다). "CRDT 없이도 수렴을 만들 수 있다"는 걸 실제 코드로 보여 주는, 학습 가치가 아주 높은 대목이다.
Electron이 무겁다는 불만 속에 Tauri(Rust 껍데기 + 시스템 웹뷰)가 대안으로 떠올랐고, reflect-open은 Tauri 2의 실전 레퍼런스다. 특히 데스크톱뿐 아니라 iOS 타깃까지 같은 React 코드로 굴리고, EventKit(캘린더)·CNContacts(연락처) 같은 애플 네이티브 API를 objc2로 브리지하는 실제 코드가 들어 있다. "Rust + 웹뷰로 크로스플랫폼 앱을 어떻게 짜는가"의 살아 있는 교재다.
냉정히 보면 reflect-open은 아직 0.7.0-beta 단계다. 그리고 Windows·Android는 아예 범위 밖(README·AGENTS.md 명시)이라 대부분의 윈도우 사용자는 지금 당장 쓸 수 없다. 텍스트 에디터의 핵심인 Meowdown은 이 저장소가 아니라 별도 저장소에 있어서, 에디터 내부까지 보려면 그쪽을 따로 봐야 한다. 즉 "완성된 대체품"이라기보다 "아키텍처를 배우기 좋은, 진행 중인 실제 제품"으로 보는 게 정확하다. 그래서 오히려 초보자가 "잘 설계된 앱의 뼈대"를 통독하기에 알맞다.
reflect-open은 pnpm/Turborepo 모노레포(JS/TS 6개 패키지) + Cargo 워크스페이스(Rust 크레이트 6개)가 한 저장소에 얽혀 있는 하이브리드 구조다. 크게 ① Rust 네이티브 층, ② React 프론트엔드 층, ③ 모두가 공유하는 SQLite 데이터 층으로 나눠 지도를 그려 보자.
| 영역 | 기술 · 버전 |
|---|---|
| UI 프레임워크 | React 19.2.7 + React Compiler(babel-plugin-react-compiler 1.0.0)로 자동 메모이제이션 |
| 빌드 · 언어 | Vite 8.1.0, TypeScript 6.0.3 — tsconfig.base.json이 극도로 엄격(exactOptionalPropertyTypes·noUncheckedIndexedAccess·verbatimModuleSyntax) |
| 상태 관리 | 비동기 읽기는 TanStack Query 5, 가벼운 로컬 상태는 React 내장 useSyncExternalStore. Redux·Zustand·Jotai·MobX 안 씀 |
| 스타일 · UI | Tailwind CSS 4.3.1 + shadcn/ui + Radix + lucide-react + cmdk + virtua(가상 리스트) + @dnd-kit |
| 검증 · ID | Zod 4.4.3(스키마 검증), react-hook-form 7.80, ulidx(정렬 가능한 ID = ULID) |
노트를 실제로 타이핑하는 리치 텍스트 에디터는 Meowdown(@meowdown/core·@meowdown/react 0.55.0)이다. 이건 Reflect 팀이 만든 라이브 프리뷰형 마크다운 에디터로, 내부는 ProseMirror(를 감싼 ProseKit) 위에 얹혀 있다. 중요한 함정: Meowdown은 이 저장소 안에 없고 별도 저장소에 산다. 그래서 에디터 자체의 버그나 내부 동작은 이 코드만 봐서는 안 나온다.
| 기능 | 크레이트 · 버전 |
|---|---|
| git 동기화 | git2 0.21(libgit2 벤더링) — GitHub·SSH 원격으로 백업/동기화 |
| 비밀 보관 | keyring 3.6.3 — API 키를 OS 키체인에 저장(env·파일·git에 절대 안 남김) |
| SQLite 쓰기 | rusqlite 0.40.1(bundled) — 색인 쓰기는 전부 Rust 트랜잭션 |
| 임베딩 | fastembed 5.13(ONNX Runtime) — all-MiniLM-L6-v2(384차원) 임베딩을 기기 안에서 계산 |
| 애플 네이티브 | objc2 0.6로 EventKit(캘린더)·CNContacts(연락처)·UIKit 브리지, notify(파일 감시), reqwest(HTTP) |
가장 흥미로운 설계다. 저장소 전체가 단 하나의 SQLite 스키마를 공유하는데, 접근하는 쪽에 따라 도구가 다르다.
| 구분 | 내용 |
|---|---|
| 읽기(TS) | Kysely 0.28.17 — 타입 안전한 쿼리 빌더. 프론트엔드가 여기서 SQL을 조립 |
| 쓰기(Rust) | rusqlite — 모든 쓰기는 index_* Rust 명령의 트랜잭션으로 |
| 의미 검색 | sqlite-vec 0.1.9(vec0 가상 테이블) + fastembed 임베딩 → 코사인 유사도 |
| 단어 검색 | FTS5 가상 테이블(SQLite 내장 전문 검색) |
| 마이그레이션 | crates/index-schema에 0001~0018 SQL 마이그레이션(데스크톱 쓰기 + CLI 읽기가 공유) |
AI는 Vercel AI SDK(ai 6.0.211) + @ai-sdk/openai·@ai-sdk/anthropic·@ai-sdk/google, OpenRouter는 OpenAI 호환 프로바이더로. CLI(reflect)는 Rust clap + pulldown-cmark(마크다운) + saphyr(YAML 프런트매터)로 today·search·show 명령을 제공한다. 크롬 확장은 WXT 0.20(MV3) + defuddle(본문 추출)로 웹페이지를 캡처한다. 빌드는 pnpm 11.9.0 + Turborepo, 린트는 oxlint.
index_* 트랜잭션만 할 수 있다. 이렇게 하면 "화면 코드가 실수로 색인을 망가뜨리는" 사고를 원천 차단할 수 있다. 이 경계가 다음 섹션의 핵심이다.reflect-open의 데이터 흐름은 한 방향의 강물처럼 흐른다. 원본(마크다운)에서 시작해 색인(SQLite)으로, 다시 화면(React)으로. 쓰기는 이 강을 거슬러 오르지 않고 별도의 수문(Rust 명령)으로만 들어간다.
프론트엔드는 SQLite에 직접 붙지 못한다(웹뷰니까). 그래서 재치 있는 다리를 놓았다(packages/db/src/dialect.ts + src-tauri/src/db/query.rs). 프론트엔드가 Kysely로 쿼리를 만들면 그게 {sql, parameters} 형태로 컴파일되고, Tauri의 db_query 명령을 타고 Rust로 건너간다. Rust가 색인에 실행해 결과 행을 돌려준다. 결정적 안전장치 — Rust 쪽은 rusqlite의 authorizer(권한 심사) API로 이 통로를 순수 읽기로 못 박아, ATTACH·DETACH·PRAGMA 같은 위험한 구문을 거부한다.
// packages/db/src/dialect.ts (주석 요지)
// 쓰기는 이 다리를 타지 않는다 — 쓰기는 index_* 명령이
// 자기 Rust 트랜잭션으로 처리한다. 그래서 이건 읽기 전용
// 다리이고, 트랜잭션은 지원하지 않는다.
이 저장소에서 가장 배울 게 많은 대목이다(apps/desktop/src-tauri/src/conflict/ladder.rs). 두 기기가 같은 노트를 따로 고쳐 충돌이 났을 때, CRDT 대신 순수 함수 하나로 푼다: "충돌한 노트 쌍"이 들어가면 → 어느 기기에서 돌리든 똑같은 바이트가 나온다. 비결은 어느 쪽이 "내 것"인지로 우선순위를 두지 않고, 두 기기가 공유하는 정보 (수정시각, 내용)으로 순서를 정하는 것이다.
// conflict/mod.rs 주석 요지
// 모든 규칙은 두 쪽을 (modified_ms, content) 로 정렬한다 —
// 두 기기가 공유하는 데이터로. "어느 쪽이 로컬인가"로는 절대 정렬 안 함.
사다리는 8단이다: ① 완전히 같음 → ② 공백만 다름 → ③ 이미 충돌 마커가 있는 쪽 유지 → ④ 병합 루프 차단 → ⑤ 공통 조상(shadow base) 기준 3-way 병합 → ⑥ 프런트매터를 키 단위로 병합 → ⑦ 데일리 노트는 append-union(이어 붙이기) → ⑧ 그래도 안 되면 라벨 붙인 충돌 마커. 위 단계에서 못 풀면 아래로 내려간다.
백링크(어떤 노트를 가리키는 다른 노트들)를 미리 저장해 두지 않는다. 대신 링크는 해소되지 않은 target_key만 저장하고, 백링크는 읽는 순간 SQL 뷰가 조인해서 만든다(migrations/0001_initial.sql).
-- 노트의 "이름·별칭·날짜"를 모두 키로 모은 뷰
CREATE VIEW note_keys AS
SELECT path, title_key AS key FROM notes
UNION SELECT note_path, alias_key FROM aliases ...;
-- 백링크 = 링크의 target_key 를 note_keys 에 조인
CREATE VIEW backlinks AS
SELECT k.note_path AS target_path, l.source_path
FROM links l JOIN note_keys k ON k.key = l.target_key;
효과가 크다 — 새 노트를 하나 만들면, 그 노트를 가리키던 링크들이 재색인 없이 즉시 백링크로 연결된다. 이름만 맞으면 조인이 알아서 이어 주기 때문이다.
그래프(=노트 폴더) 하나는 iCloud Drive 또는 git 원격 중 하나로만 동기화한다(둘 다 동시에는 안 됨). git 경로는 커밋·브랜치 같은 git 용어를 사용자에게 숨기고 "백업됨/백업 중/오프라인/검토 필요/백업 실패"라는 제품 상태로만 보여 준다. iCloud 경로는 동시 편집을 NSFileVersion으로 감지해 앞서 본 결정론적 사다리로 기기 안에서 푼다. 서버가 개입하지 않는, 진짜 클라이언트 온리 구조다(이 저장소엔 백엔드 서버 코드가 없다).
규모를 숫자로 보면 — 추적 파일 총 1,578개, TS/TSX 약 131,000줄(978파일), Rust 약 24,400줄(97파일), Swift 약 1,800줄(15파일), SQL 마이그레이션 18개. Rust↔JS를 잇는 #[tauri::command] 핸들러가 99개. 눈여겨볼 세 곳은 ① conflict/ladder.rs(충돌 해소), ② packages/db/src/dialect.ts(읽기 전용 다리), ③ crates/index-schema/migrations/0001_initial.sql(데이터 모델)이다.
이 저장소를 처음 열면 방대해 보이지만, 길잡이가 친절하다. docs/plans/에 00~23까지 번호 매긴 구현 계획이 기능과 1:1로 대응한다 — 04=색인, 07=백링크, 09=임베딩, 10=AI 코파일럿, 11=캡처, 12=동기화, 14=CLI, 19=모바일, 21=iCloud. "이 기능은 어디 있지?"가 궁금하면 코드를 뒤지기 전에 해당 번호의 plan 문서부터 읽는 게 지름길이다.
분산 시스템에서 두 복제본을 다투지 않게 합치는 정석은 CRDT다. 그런데 CRDT는 무겁고, 메타데이터가 계속 쌓이며, 마크다운 같은 "그냥 텍스트"에 얹기가 까다롭다. reflect-open은 순수 함수 + 결정론이라는 더 단순한 무기로 같은 목표(수렴)를 노린다. 핵심 통찰은 하나다 — "누가 로컬인가"라는, 기기마다 다른 정보로 판단하지 말 것. 대신 (수정시각, 내용)처럼 두 기기가 똑같이 아는 정보로만 순서를 정하면, 어느 기기가 먼저 병합해도 결과가 같아진다. 이 아이디어는 노트 앱을 넘어 어떤 "오프라인 편집 후 합치기" 문제에도 응용할 수 있다.
"프론트엔드는 읽기만, 쓰기는 백엔드만"이라는 규칙을 문서로만 정해 두면 언젠가 깨진다. reflect-open은 이걸 기계가 강제하게 만들었다 — Kysely로 만든 SQL은 db_query 통로로만 가고, 그 통로는 rusqlite authorizer가 SELECT 외 구문을 런타임에 거부한다. "설계 원칙을 주석이 아니라 실행 가능한 방어선으로 바꾸는" 좋은 예다. 웹뷰↔네이티브를 나누는 모든 앱(Electron·Tauri)에 그대로 옮길 만한 패턴이다.
백링크를 저장해 두면, 노트 이름을 바꾸거나 새 노트를 만들 때마다 관련 링크를 전부 찾아 갱신해야 한다(비싸고 버그가 많다). reflect-open은 아예 저장을 안 한다. 링크는 "가리키려는 이름(target_key)"만 들고 있고, 백링크는 SQL 뷰가 읽는 순간 조인으로 계산한다. "미리 계산(eager) vs 필요할 때 계산(lazy)"의 트레이드오프를 실물로 배우는 대목이다 — 여기선 lazy가 훨씬 깔끔하다.
의미 검색("비슷한 노트 찾기")을 하려면 노트를 임베딩(벡터)으로 바꿔야 하는데, 보통은 이걸 OpenAI 서버에 보낸다 — 프라이버시 문제가 생긴다. reflect-open은 embed.rs에서 fastembed로 MiniLM 모델을 기기에 내려받아 로컬에서 임베딩을 만든다(GPU 없이 CPU로). 벡터는 sqlite-vec의 vec0 테이블에 넣어 코사인 유사도로 검색한다. "AI 기능을 넣되 데이터는 기기 밖으로 안 내보내는" 실전 설계를 통째로 볼 수 있다. 게다가 private: true가 붙은 노트는 모든 외부 호출에서 아예 제외하는 게이트(ai/checkers.ts)까지 있다.
이 저장소가 주는 가장 큰 교훈은 화려한 알고리즘이 아니라 데이터 설계 철학이다 — 원본(마크다운)과 캐시(SQLite 색인)를 물리적으로 갈라 놓고, 캐시는 "언제든 버리고 다시 만들 수 있는 것"으로 취급한다. 이 원칙 하나면 백업·동기화·복구가 전부 단순해진다(원본만 지키면 되니까). 내 프로젝트에도 "무엇이 진실원이고, 무엇이 파생 캐시인가"를 한 줄로 답할 수 있게 만드는 것 — 그게 여기서 가져갈 핵심이다.
| 목적 | 요구사항 |
|---|---|
| 기본 도구 | Node 22(CI 고정), pnpm 11.9.0(corepack), 안정판 Rust 툴체인, Xcode Command Line Tools(Tauri/macOS 빌드) |
| 플랫폼 | macOS + iOS 전용. Windows·Android·플러그인 API는 명시적으로 범위 밖(README·AGENTS.md) |
| 데이터베이스 | 설정 불필요 — SQLite 색인은 <그래프>/.reflect/ 아래 자동 생성·마이그레이션 |
| API 키 | 실행에 키 불필요. AI는 BYOK로 OS 키체인에 저장(env·마크다운·git엔 안 남음). .env.example도 없음 |
| GPU | 불필요 — 온디바이스 임베딩(ONNX)은 CPU로 동작 |
| 환경변수(선택) | VITE_SENTRY_DSN(텔레메트리, 없으면 꺼짐), HF_HOME/HF_ENDPOINT(임베딩 모델 캐시·미러), REFLECT_GRAPH(CLI 그래프 지정) |
# 준비
corepack enable && pnpm install
# 전체 앱 실행 (CLI 사이드카를 먼저 스테이징)
pnpm tauri dev
# 화면(Vite)만 → http://localhost:1420 (?platform=ios 로 모바일 미리보기)
pnpm dev
# iOS 시뮬레이터
pnpm tauri:ios:dev "iPhone 17 Pro"
# 타입체크 + 린트
pnpm check
reflect-open은 클라우드 인프라가 전혀 필요 없다. 회원가입도, 데이터베이스 서버도, GPU 팜도 없다. AI를 쓰고 싶으면 내 API 키를 키체인에 한 번 넣으면 되고, 안 써도 노트·검색·백링크는 다 돌아간다. 진입 장벽의 유일한 벽은 "애플 기기여야 한다"는 것뿐 — Windows 사용자는 지금은 소스를 읽는 용도로만 접근 가능하다는 점을 기억하자.
그래프 폴더에서 notes/에 .md 파일을 에디터가 아니라 그냥 텍스트 편집기로 하나 만들어 본다. 앱을 켜면 그 노트가 나타나는가? 이번엔 .reflect/index.sqlite를 통째로 지우고 앱을 재시작해, 노트가 그대로 살아 있고 검색만 다시 빌드되는지 관찰한다. "원본 vs 캐시"를 몸으로 이해하는 게 목표.
노트 A에 [[아직없는노트]]라고 링크를 쓴다(대상은 아직 없음). 그다음 아직없는노트.md를 만들자마자 A가 백링크로 자동 연결되는지 확인한다. 재색인 버튼을 누르지 않았는데도 연결된다면, 그게 바로 "쿼리 시점 계산"의 효과다. migrations/0001_initial.sql의 backlinks 뷰와 대조해 보라.
conflict/ladder.rs의 8단 중 앞의 3~4단(완전 일치 / 공백만 다름 / (수정시각, 내용)으로 정렬)을 파이썬이나 JS로 옮겨 작은 함수로 만든다. 그리고 "같은 입력 쌍을 순서만 바꿔 넣어도 결과가 같은지" 테스트를 짠다. 결정론이 왜 수렴을 만드는지를 코드로 증명하는 게 핵심.
작은 Tauri(또는 Electron) 앱에서 "프론트엔드가 SQL 문자열을 만들면 백엔드가 실행"하는 통로를 만들되, 백엔드에서 INSERT/UPDATE를 거부하는 심사기를 넣어 본다. reflect-open처럼 authorizer로 막는 게 이상적이지만, 없으면 SQL 앞 단어를 검사하는 것부터 시작하자. "신뢰 경계를 실행으로 강제한다"는 감각을 익힌다.
.md 폴더를 감시(watch)하다가 파일이 바뀌면 SQLite(FTS5)에 제목·본문·[[링크]]를 색인하는 작은 색인기를 직접 짠다. 여력이 되면 sqlite-vec + 온디바이스 임베딩으로 의미 검색까지 얹어 본다. reflect-open의 watcher.rs·embed.rs가 참고 예시. "파일 → 색인 → 검색" 파이프라인을 스스로 완성하는 종합 과제다.
| 주차 | 주제 · 학습 목표 |
|---|---|
| 1주차 Tauri · 로컬 우선 | Tauri 2와 로컬 우선 설계를 이해한다. Rust 껍데기 + 시스템 웹뷰 구조, #[tauri::command]로 웹↔네이티브 잇기, 왜 Electron 대신 Tauri인가. "로컬 우선"의 4대 원칙(내 기기가 원본·오프라인 동작·동기화는 부가·앱이 사라져도 데이터 잔존)을 reflect-open이 마크다운으로 어떻게 구현했는지 대조. |
| 2주차 SQLite 투영 · IPC | "원본 vs 캐시" 데이터 설계와 안전한 IPC. 마크다운을 진실원으로 두고 SQLite를 투영본으로 만드는 색인 파이프라인(watcher.rs), Kysely(TS 읽기)와 rusqlite(Rust 쓰기)의 역할 분리, authorizer로 읽기 전용 경계를 런타임 강제하기. FTS5 전문 검색도 함께. |
| 3주차 충돌 해소 · 동기화 | CRDT 없이 수렴 만들기. 결정론적 병합의 원리(공유 정보로만 정렬), 3-way 병합과 공통 조상(shadow base), conflict/ladder.rs의 8단 사다리 정독. git 동기화를 제품 상태로 추상화하는 법, iCloud NSFileVersion 충돌 처리. CRDT(Yjs·Automerge)와의 트레이드오프 비교. |
| 4주차 온디바이스 AI | 프라이버시 보존형 AI. 임베딩·벡터 검색의 기초, sqlite-vec로 SQLite에 벡터 얹기, fastembed(ONNX)로 기기 안에서 MiniLM 돌리기, RAG("내 노트에게 묻기")와 private:true 프라이버시 게이트. BYOK로 여러 프로바이더(OpenAI·Anthropic·Google)를 Vercel AI SDK로 추상화하기. |
.md 파일로 둬서 이를 극단까지 밀었다..md 파일이 결정한다는 원칙. 앱·색인·화면은 전부 이 파일에서 파생된다..reflect/index.sqlite처럼 원본에서 언제든 다시 만들 수 있는 파생 사본. 지워도 색인기가 파일에서 복원한다(예외: AI 대화 chat_*).conflict/ladder.rs의 8단 규칙. 위 단계에서 못 풀면 아래로 내려가며, 마지막은 라벨 붙은 충돌 마커. 어느 기기서 돌려도 결과가 같다.db_query로 Rust에 넘겨 실행. rusqlite authorizer가 쓰기·위험 구문을 거부해 순수 읽기로 못 박는다.target_key를 SQL 뷰가 읽는 순간 조인해 계산한다 → 재색인 없이 즉시 연결.sqlite-vec의 vec0 테이블에서 코사인 유사도로 검색.daily/·notes/·.reflect/가 들어 있다.저장소 · 제품
· GitHub: github.com/team-reflect/reflect-open (MIT © 2026 Reflect App, LLC · 앱 v0.7.0-beta.8)
· 상용 제품: reflect.app (이 저장소는 그 오픈소스 재작성판 V2)
· 최신 커밋: a30a8aa (2026-07-17, "chore: release 0.7.0-beta.8")
핵심 파일(클론 후)
· AGENTS.md — 저장소의 권위 있는 개요(V2 철학·규칙)
· apps/desktop/src-tauri/src/conflict/ladder.rs — 결정론적 충돌 사다리
· packages/db/src/dialect.ts + src-tauri/src/db/query.rs — 읽기 전용 IPC 다리
· crates/index-schema/migrations/0001_initial.sql — 데이터 모델(백링크 뷰)
· apps/desktop/src-tauri/src/embed.rs — 온디바이스 임베딩
· docs/plans/ — 00~23 기능별 구현 계획(지도)
배경 지식
· Tauri 2 문서 · Kysely · sqlite-vec
· Local-first software (Ink & Switch) — 로컬 우선 철학의 원전
· ProseMirror(Meowdown의 토대) · fastembed
① Electron/Next.js 아님 — Tauri 2(Rust + 웹뷰)다. ② 버전 진실원은 apps/desktop/package.json=0.7.0-beta.8 — 루트·패키지의 0.1.0, Rust 크레이트의 0.0.0(의도적 동결)은 제품 버전이 아니다. ③ 에디터 Meowdown은 외부 저장소 — 이 코드만으론 에디터 내부가 안 보인다. ④ macOS·iOS 전용 — Windows·Android는 범위 밖(keyring의 windows 기능은 열망일 뿐). ⑤ SQLite는 재빌드 가능 — 단 chat_*(AI 대화)만은 파일로 못 만드니 영구 보존. ⑥ AI 모델 카탈로그는 미래형 이름(gpt-5.6·claude-opus-4-8·gemini-3.5-flash 등, ai/provider-catalog.ts) — 2026년 기준 정적 목록이라 실존 여부는 별도 확인. 덤으로 README는 "pnpm 10"이라 적었지만 packageManager는 pnpm@11.9.0이다.