TrendShift 딥다이브 · 2026-07-17 · Daily #17

team-reflect/reflect-open 딥다이브
— 노트 앱을 "마크다운 파일 + Tauri + 로컬 우선"으로 다시 짠 오픈소스

reflect-open은 상용 노트 앱 Reflect(reflect.app)를 "내 디스크의 마크다운 파일이 원본이고, 계정도 회사 서버도 없는 로컬 우선(local-first) 앱"으로 처음부터 다시 짠 오픈소스 재작성판(=Reflect V2)이다. 여기서 가장 먼저 오해를 풀어야 할 게 있다 — 이건 Electron도 Next.js도 아닌 Tauri 2(Rust 껍데기 + React 19 웹뷰) 데스크톱/모바일 앱이다. 두 번째로 각인할 발상 — 노트는 그냥 .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)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 시스템 · 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

상용 노트 앱을 "마크다운 파일이 진실원"인 로컬 우선 오픈소스로 다시 짰다

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는 지워도 파일에서 다시 만들어지는 사본일 뿐이다.

핵심 용어 · 이게 뭐냐
로컬 우선(Local-first) — 내 기기가 원본, 클라우드는 거들 뿐
로컬 우선은 데이터의 진짜 원본이 내 기기에 있고, 인터넷이 끊겨도 100% 동작하며, 동기화는 "있으면 좋은 부가 기능"으로 두는 설계 철학이다. Notion·구글독스처럼 "서버에 원본이 있고 내 화면은 그걸 잠깐 비추는 창"인 클라우드 우선과 정반대다. reflect-open은 여기서 한 발 더 나가 원본을 앱 전용 포맷이 아니라 누구나 읽는 .md 파일로 둬서, 앱이 사라져도 노트는 그대로 남는다.
핵심 용어 · 이 프로젝트의 심장
마크다운 진실원 + 투영본(Projection)
reflect-open의 데이터에는 두 층이 있다. ① 진실원(source of truth) = 디스크의 .md 파일들. 무엇이 옳은지는 오직 이 파일이 결정한다. ② 투영본 = <그래프>/.reflect/index.sqlite. 파일들을 빠르게 뒤지려고 만든 색인 사본이다. 투영본은 언제든 지우고 파일에서 다시 빌드할 수 있다(단 하나의 예외: AI 대화 기록 chat_* 테이블만은 파일에서 못 만들어 내니 보존한다). "원본과 캐시를 분명히 갈라 놓는" 이 구조가 앱 전체 설계를 관통한다.
한 문장 비유

"클라우드 금고에 맡기던 노트를 내 책상 서랍 속 종이 노트로 되돌리되, 색인 카드는 로봇이 자동으로 다시 만들어 주는 서재"

Notion 같은 앱은 은행 금고와 같다 — 내 노트가 회사 서버에 있고, 나는 열쇠(계정)로 잠깐 열어 볼 뿐이다. 회사가 문을 닫으면 금고째 사라진다. reflect-open은 노트를 다시 내 서랍(디스크의 .md 파일)으로 가져온다. 앱이 없어져도 서랍 속 종이는 그대로다.

그런데 종이 노트의 약점은 "검색이 느리다"는 것. 그래서 이 서재엔 사서 로봇이 하나 있다. 서랍 내용이 바뀔 때마다 로봇(Rust 색인기)이 색인 카드(SQLite)를 자동으로 다시 만든다. 색인 카드를 통째로 태워 버려도, 로봇이 서랍을 다시 훑어 카드를 복원한다 — 원본은 언제나 서랍 속 종이다.

2왜 지금 주목받는가

TrendShift Daily #17 — "상용 노트 앱의 전면 오픈소스화 + CRDT를 버린 결정론적 병합 + Tauri 2 실전 표본"

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 — 실제 상용 제품을 운영하는 회사가 직접 재작성판을 오픈소스로 냈다.

주목 포인트 1 — 상용 앱을 통째로, 그것도 "다시 써서" 열었다

보통 상용 서비스가 오픈소스를 낼 때는 SDK나 일부 모듈만 공개한다. reflect-open은 다르다. macOS 앱 + iOS 앱(같은 코드베이스) + 크롬 확장 + 네이티브 메시징 호스트 + CLI까지, 제품을 이루는 거의 전부가 들어 있다. 게다가 기존 코드를 그대로 던진 게 아니라 철학을 바꿔 다시 썼다 — 파이어베이스(클라우드 DB) 기반이던 것을 "파일이 원본"으로, Electron/Next.js를 Tauri로, Yjs를 결정론적 병합으로. "제품을 리라이트하며 아키텍처를 갈아엎는" 실제 사례를 통째로 관찰할 수 있다.

주목 포인트 2 — "로컬 우선인데 CRDT를 안 쓴다"는 기술적 도발

2020년대 로컬 우선 앱의 정석은 Yjs·Automerge 같은 CRDT(충돌 없이 자동 병합되는 자료구조)다. 그런데 reflect-open V2는 이걸 의도적으로 걷어냈다. 대신 conflict/ladder.rs결정론적 충돌 해소 사다리로, 같은 충돌은 어느 기기에서 풀든 똑같은 결과 바이트가 나오게 만든다. 그래서 두 기기가 같은 충돌을 동시에 풀어도 결과가 수렴한다(핑퐁이 안 생긴다). "CRDT 없이도 수렴을 만들 수 있다"는 걸 실제 코드로 보여 주는, 학습 가치가 아주 높은 대목이다.

주목 포인트 3 — Tauri 2로 mac·iOS를 한 코드베이스로

Electron이 무겁다는 불만 속에 Tauri(Rust 껍데기 + 시스템 웹뷰)가 대안으로 떠올랐고, reflect-open은 Tauri 2의 실전 레퍼런스다. 특히 데스크톱뿐 아니라 iOS 타깃까지 같은 React 코드로 굴리고, EventKit(캘린더)·CNContacts(연락처) 같은 애플 네이티브 API를 objc2로 브리지하는 실제 코드가 들어 있다. "Rust + 웹뷰로 크로스플랫폼 앱을 어떻게 짜는가"의 살아 있는 교재다.

균형 잡힌 시각
과대평가 금물 — 아직 beta이고, macOS·iOS 전용이다

냉정히 보면 reflect-open은 아직 0.7.0-beta 단계다. 그리고 Windows·Android는 아예 범위 밖(README·AGENTS.md 명시)이라 대부분의 윈도우 사용자는 지금 당장 쓸 수 없다. 텍스트 에디터의 핵심인 Meowdown은 이 저장소가 아니라 별도 저장소에 있어서, 에디터 내부까지 보려면 그쪽을 따로 봐야 한다. 즉 "완성된 대체품"이라기보다 "아키텍처를 배우기 좋은, 진행 중인 실제 제품"으로 보는 게 정확하다. 그래서 오히려 초보자가 "잘 설계된 앱의 뼈대"를 통독하기에 알맞다.

3기술 스택 전체 지도

껍데기 = Rust(Tauri 2) · 화면 = React 19 · 데이터 = SQLite 하나(TS는 읽기, Rust는 쓰기)

reflect-open은 pnpm/Turborepo 모노레포(JS/TS 6개 패키지) + Cargo 워크스페이스(Rust 크레이트 6개)가 한 저장소에 얽혀 있는 하이브리드 구조다. 크게 ① Rust 네이티브 층, ② React 프론트엔드 층, ③ 모두가 공유하는 SQLite 데이터 층으로 나눠 지도를 그려 보자.

① 프론트엔드 — React 19 + Vite 8 + Tailwind 4 (상태 관리는 "가볍게")

영역기술 · 버전
UI 프레임워크React 19.2.7 + React Compiler(babel-plugin-react-compiler 1.0.0)로 자동 메모이제이션
빌드 · 언어Vite 8.1.0, TypeScript 6.0.3tsconfig.base.json극도로 엄격(exactOptionalPropertyTypes·noUncheckedIndexedAccess·verbatimModuleSyntax)
상태 관리비동기 읽기는 TanStack Query 5, 가벼운 로컬 상태는 React 내장 useSyncExternalStore. Redux·Zustand·Jotai·MobX 안 씀
스타일 · UITailwind CSS 4.3.1 + shadcn/ui + Radix + lucide-react + cmdk + virtua(가상 리스트) + @dnd-kit
검증 · IDZod 4.4.3(스키마 검증), react-hook-form 7.80, ulidx(정렬 가능한 ID = ULID)

② 에디터 — Meowdown (ProseMirror 기반, 단 외부 저장소)

노트를 실제로 타이핑하는 리치 텍스트 에디터는 Meowdown(@meowdown/core·@meowdown/react 0.55.0)이다. 이건 Reflect 팀이 만든 라이브 프리뷰형 마크다운 에디터로, 내부는 ProseMirror(를 감싼 ProseKit) 위에 얹혀 있다. 중요한 함정: Meowdown은 이 저장소 안에 없고 별도 저장소에 산다. 그래서 에디터 자체의 버그나 내부 동작은 이 코드만 봐서는 안 나온다.

③ 네이티브(Rust) — 파일·git·암호·임베딩을 전부 Rust가

기능크레이트 · 버전
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 하나"에 세 가지 검색을 얹다

가장 흥미로운 설계다. 저장소 전체가 단 하나의 SQLite 스키마를 공유하는데, 접근하는 쪽에 따라 도구가 다르다.

구분내용
읽기(TS)Kysely 0.28.17 — 타입 안전한 쿼리 빌더. 프론트엔드가 여기서 SQL을 조립
쓰기(Rust)rusqlite — 모든 쓰기는 index_* Rust 명령의 트랜잭션으로
의미 검색sqlite-vec 0.1.9(vec0 가상 테이블) + fastembed 임베딩 → 코사인 유사도
단어 검색FTS5 가상 테이블(SQLite 내장 전문 검색)
마이그레이션crates/index-schema0001~0018 SQL 마이그레이션(데스크톱 쓰기 + CLI 읽기가 공유)

⑤ AI · CLI · 확장

AIVercel 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.

핵심 용어 · 왜 "TS는 읽기, Rust는 쓰기"인가
Kysely(읽기) vs rusqlite(쓰기) — 역할을 물리적으로 갈랐다
같은 SQLite를 두 언어가 만지지만 역할이 다르다. 프론트엔드(TS)는 절대 직접 쓰지 않고 읽기만 한다 — Kysely로 SQL을 만들어 Rust에 "이 SELECT 좀 실행해 줘"라고 넘긴다. 실제 파일 변경(쓰기)은 오직 Rust의 index_* 트랜잭션만 할 수 있다. 이렇게 하면 "화면 코드가 실수로 색인을 망가뜨리는" 사고를 원천 차단할 수 있다. 이 경계가 다음 섹션의 핵심이다.

4아키텍처 심화 분석

마크다운 → Rust 색인기 → SQLite 투영본 → React 읽기, 그리고 "읽기 전용 IPC 다리"

reflect-open의 데이터 흐름은 한 방향의 강물처럼 흐른다. 원본(마크다운)에서 시작해 색인(SQLite)으로, 다시 화면(React)으로. 쓰기는 이 강을 거슬러 오르지 않고 별도의 수문(Rust 명령)으로만 들어간다.

┌──────────────────────────────────────────────────────────────┐ │ reflect-open 데이터 흐름 │ └──────────────────────────────────────────────────────────────┘ [ 진실원 ] [ 색인기 ] [ 투영본 ] 마크다운 .md 파일 ──► Rust 감시자 ──► .reflect/index.sqlite daily/2026-..md (watcher.rs) (지웠다 다시 만들 수 있음) notes/*.md notify로 변경 감지 │ ▲ │ 읽기(SELECT) │ 쓰기는 여기로만 ▼ │ (index_* Rust 트랜잭션) ┌──────────────────┐ │ │ React 19 웹뷰 │ └───────────────────────────────────────│ Kysely로 쿼리 조립 │ db_query (읽기 전용 IPC) └──────────────────┘ ※ 예외: chat_* (AI 대화)만은 파일로 못 만드니 SQLite에 영구 보존

핵심 설계 1 — 읽기 전용 IPC SQLite 다리

프론트엔드는 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 트랜잭션으로 처리한다. 그래서 이건 읽기 전용
// 다리이고, 트랜잭션은 지원하지 않는다.

핵심 설계 2 — CRDT 없는 "결정론적 충돌 사다리"

이 저장소에서 가장 배울 게 많은 대목이다(apps/desktop/src-tauri/src/conflict/ladder.rs). 두 기기가 같은 노트를 따로 고쳐 충돌이 났을 때, CRDT 대신 순수 함수 하나로 푼다: "충돌한 노트 쌍"이 들어가면 → 어느 기기에서 돌리든 똑같은 바이트가 나온다. 비결은 어느 쪽이 "내 것"인지로 우선순위를 두지 않고, 두 기기가 공유하는 정보 (수정시각, 내용)으로 순서를 정하는 것이다.

// conflict/mod.rs 주석 요지
// 모든 규칙은 두 쪽을 (modified_ms, content) 로 정렬한다 —
// 두 기기가 공유하는 데이터로. "어느 쪽이 로컬인가"로는 절대 정렬 안 함.

사다리는 8단이다: ① 완전히 같음 → ② 공백만 다름 → ③ 이미 충돌 마커가 있는 쪽 유지 → ④ 병합 루프 차단 → ⑤ 공통 조상(shadow base) 기준 3-way 병합 → ⑥ 프런트매터를 키 단위로 병합 → ⑦ 데일리 노트는 append-union(이어 붙이기) → ⑧ 그래도 안 되면 라벨 붙인 충돌 마커. 위 단계에서 못 풀면 아래로 내려간다.

핵심 설계 3 — 쿼리 시점에 백링크를 "계산"한다

백링크(어떤 노트를 가리키는 다른 노트들)를 미리 저장해 두지 않는다. 대신 링크는 해소되지 않은 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;

효과가 크다 — 새 노트를 하나 만들면, 그 노트를 가리키던 링크들이 재색인 없이 즉시 백링크로 연결된다. 이름만 맞으면 조인이 알아서 이어 주기 때문이다.

핵심 설계 4 — 동기화는 iCloud 또는 git, 둘 중 하나만

그래프(=노트 폴더) 하나는 iCloud Drive 또는 git 원격 중 하나로만 동기화한다(둘 다 동시에는 안 됨). git 경로는 커밋·브랜치 같은 git 용어를 사용자에게 숨기고 "백업됨/백업 중/오프라인/검토 필요/백업 실패"라는 제품 상태로만 보여 준다. iCloud 경로는 동시 편집을 NSFileVersion으로 감지해 앞서 본 결정론적 사다리로 기기 안에서 푼다. 서버가 개입하지 않는, 진짜 클라이언트 온리 구조다(이 저장소엔 백엔드 서버 코드가 없다).

5디렉토리 구조 해부

apps/(앱 4개) · packages/(공유 TS 3개) · crates/(공유 스키마) — 모노레포 지도
reflect-open/ ├── apps/ │ ├── desktop/ # @reflect/desktop — Tauri 2 mac + iOS 앱 (핵심) │ │ ├── src/ # React 프론트: app.tsx, editor/, components/, hooks/, mobile/ │ │ └── src-tauri/ # Rust 크레이트: lib.rs, db/, fs/, git/, icloud/, │ │ # conflict/(충돌 사다리), watcher.rs, embed.rs(임베딩), secrets.rs │ ├── cli/ # reflect CLI (Rust): today/search/show/path/open │ ├── extension/ # @reflect/extension — 크롬 MV3 캡처 (WXT) │ └── native-host/ # reflect-capture-host — 크롬 네이티브 메시징 (Rust) ├── packages/ │ ├── core/ # @reflect/core — 공유 TS 로직 전부 (가장 큼) │ │ # markdown/ indexing/(73파일) graph/ embeddings/ ai/(45파일) sync/ │ ├── db/ # @reflect/db — Kysely 스키마 + "읽기 전용 IPC 다리" │ └── utils/ # @reflect/utils ├── crates/ │ └── index-schema/ # reflect-index-schema — SQLite 마이그레이션 0001~0018# (데스크톱 쓰기 + CLI 읽기가 공유하는 단일 스키마) ├── design-system/ # 디자인 토큰·컴포넌트·가이드라인 ├── plugins/ # tauri-plugin-keyboard, tauri-plugin-recording (iOS Swift+Rust) ├── docs/ # 제품·아키텍처 문서 + docs/plans/ 00~23 (기능별 구현 계획) ├── Cargo.toml / package.json / turbo.json / pnpm-workspace.yaml

규모를 숫자로 보면 — 추적 파일 총 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 가 곧 지도

이 저장소를 처음 열면 방대해 보이지만, 길잡이가 친절하다. docs/plans/00~23까지 번호 매긴 구현 계획이 기능과 1:1로 대응한다 — 04=색인, 07=백링크, 09=임베딩, 10=AI 코파일럿, 11=캡처, 12=동기화, 14=CLI, 19=모바일, 21=iCloud. "이 기능은 어디 있지?"가 궁금하면 코드를 뒤지기 전에 해당 번호의 plan 문서부터 읽는 게 지름길이다.

6학습 포인트

이 저장소에서만 배울 수 있는 네 가지 — 수렴·경계·지연 계산·온디바이스

배울 것 1 — CRDT 없이 "수렴"을 만드는 결정론적 병합

분산 시스템에서 두 복제본을 다투지 않게 합치는 정석은 CRDT다. 그런데 CRDT는 무겁고, 메타데이터가 계속 쌓이며, 마크다운 같은 "그냥 텍스트"에 얹기가 까다롭다. reflect-open은 순수 함수 + 결정론이라는 더 단순한 무기로 같은 목표(수렴)를 노린다. 핵심 통찰은 하나다 — "누가 로컬인가"라는, 기기마다 다른 정보로 판단하지 말 것. 대신 (수정시각, 내용)처럼 두 기기가 똑같이 아는 정보로만 순서를 정하면, 어느 기기가 먼저 병합해도 결과가 같아진다. 이 아이디어는 노트 앱을 넘어 어떤 "오프라인 편집 후 합치기" 문제에도 응용할 수 있다.

배울 것 2 — 신뢰 경계를 코드로 강제하기 (읽기 전용 IPC)

"프론트엔드는 읽기만, 쓰기는 백엔드만"이라는 규칙을 문서로만 정해 두면 언젠가 깨진다. reflect-open은 이걸 기계가 강제하게 만들었다 — Kysely로 만든 SQL은 db_query 통로로만 가고, 그 통로는 rusqlite authorizerSELECT 외 구문을 런타임에 거부한다. "설계 원칙을 주석이 아니라 실행 가능한 방어선으로 바꾸는" 좋은 예다. 웹뷰↔네이티브를 나누는 모든 앱(Electron·Tauri)에 그대로 옮길 만한 패턴이다.

배울 것 3 — 미리 만들지 말고 "읽을 때 계산"하기 (지연 백링크)

백링크를 저장해 두면, 노트 이름을 바꾸거나 새 노트를 만들 때마다 관련 링크를 전부 찾아 갱신해야 한다(비싸고 버그가 많다). reflect-open은 아예 저장을 안 한다. 링크는 "가리키려는 이름(target_key)"만 들고 있고, 백링크는 SQL 뷰가 읽는 순간 조인으로 계산한다. "미리 계산(eager) vs 필요할 때 계산(lazy)"의 트레이드오프를 실물로 배우는 대목이다 — 여기선 lazy가 훨씬 깔끔하다.

배울 것 4 — 프라이버시를 지키는 온디바이스 AI

의미 검색("비슷한 노트 찾기")을 하려면 노트를 임베딩(벡터)으로 바꿔야 하는데, 보통은 이걸 OpenAI 서버에 보낸다 — 프라이버시 문제가 생긴다. reflect-open은 embed.rs에서 fastembed로 MiniLM 모델을 기기에 내려받아 로컬에서 임베딩을 만든다(GPU 없이 CPU로). 벡터는 sqlite-vecvec0 테이블에 넣어 코사인 유사도로 검색한다. "AI 기능을 넣되 데이터는 기기 밖으로 안 내보내는" 실전 설계를 통째로 볼 수 있다. 게다가 private: true가 붙은 노트는 모든 외부 호출에서 아예 제외하는 게이트(ai/checkers.ts)까지 있다.

그대로 훔쳐 쓸 발상 · 초보자용 정리
"원본과 캐시를 분명히 나눠라"

이 저장소가 주는 가장 큰 교훈은 화려한 알고리즘이 아니라 데이터 설계 철학이다 — 원본(마크다운)캐시(SQLite 색인)를 물리적으로 갈라 놓고, 캐시는 "언제든 버리고 다시 만들 수 있는 것"으로 취급한다. 이 원칙 하나면 백업·동기화·복구가 전부 단순해진다(원본만 지키면 되니까). 내 프로젝트에도 "무엇이 진실원이고, 무엇이 파생 캐시인가"를 한 줄로 답할 수 있게 만드는 것 — 그게 여기서 가져갈 핵심이다.

7시스템 · 실행 요구사항

Node 22 + pnpm 11.9 + Rust + Xcode — 단 macOS·iOS 전용, API 키 없이도 실행
목적요구사항
기본 도구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 사용자는 지금은 소스를 읽는 용도로만 접근 가능하다는 점을 기억하자.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 파일 실험부터 충돌 사다리 재현·나만의 색인기까지
난이도 ★☆☆ · 초급

과제 1 — "마크다운이 진실원"임을 손으로 확인

그래프 폴더에서 notes/.md 파일을 에디터가 아니라 그냥 텍스트 편집기로 하나 만들어 본다. 앱을 켜면 그 노트가 나타나는가? 이번엔 .reflect/index.sqlite를 통째로 지우고 앱을 재시작해, 노트가 그대로 살아 있고 검색만 다시 빌드되는지 관찰한다. "원본 vs 캐시"를 몸으로 이해하는 게 목표.

난이도 ★☆☆ · 초급

과제 2 — 백링크가 "즉시" 이어지는지 실험

노트 A에 [[아직없는노트]]라고 링크를 쓴다(대상은 아직 없음). 그다음 아직없는노트.md를 만들자마자 A가 백링크로 자동 연결되는지 확인한다. 재색인 버튼을 누르지 않았는데도 연결된다면, 그게 바로 "쿼리 시점 계산"의 효과다. migrations/0001_initial.sqlbacklinks 뷰와 대조해 보라.

난이도 ★★☆ · 중급

과제 3 — 결정론적 충돌 사다리를 다른 언어로 재현

conflict/ladder.rs의 8단 중 앞의 3~4단(완전 일치 / 공백만 다름 / (수정시각, 내용)으로 정렬)을 파이썬이나 JS로 옮겨 작은 함수로 만든다. 그리고 "같은 입력 쌍을 순서만 바꿔 넣어도 결과가 같은지" 테스트를 짠다. 결정론이 왜 수렴을 만드는지를 코드로 증명하는 게 핵심.

난이도 ★★☆ · 중급

과제 4 — 읽기 전용 IPC 다리 흉내 내기

작은 Tauri(또는 Electron) 앱에서 "프론트엔드가 SQL 문자열을 만들면 백엔드가 실행"하는 통로를 만들되, 백엔드에서 INSERT/UPDATE를 거부하는 심사기를 넣어 본다. reflect-open처럼 authorizer로 막는 게 이상적이지만, 없으면 SQL 앞 단어를 검사하는 것부터 시작하자. "신뢰 경계를 실행으로 강제한다"는 감각을 익힌다.

난이도 ★★★ · 고급

과제 5 — 마크다운 폴더용 미니 색인기 만들기

.md 폴더를 감시(watch)하다가 파일이 바뀌면 SQLite(FTS5)에 제목·본문·[[링크]]를 색인하는 작은 색인기를 직접 짠다. 여력이 되면 sqlite-vec + 온디바이스 임베딩으로 의미 검색까지 얹어 본다. reflect-open의 watcher.rs·embed.rs가 참고 예시. "파일 → 색인 → 검색" 파이프라인을 스스로 완성하는 종합 과제다.

9관련 기술 심화 학습 로드맵

4주 코스 — Tauri/로컬 우선 → SQLite 투영·IPC → 충돌 해소·동기화 → 온디바이스 AI
주차주제 · 학습 목표
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로 추상화하기.

10핵심 키워드 사전

이 문서에서 반드시 챙길 용어 14개
용어 01
로컬 우선(Local-first)
데이터의 원본이 내 기기에 있고, 오프라인에서 완전히 동작하며, 동기화는 부가 기능인 설계 철학. reflect-open은 원본을 앱 전용 DB가 아니라 .md 파일로 둬서 이를 극단까지 밀었다.
용어 02
마크다운 진실원(Markdown-as-truth)
무엇이 옳은 데이터인지를 오직 디스크의 .md 파일이 결정한다는 원칙. 앱·색인·화면은 전부 이 파일에서 파생된다.
용어 03
투영본(Projection) · 재빌드 가능한 캐시
.reflect/index.sqlite처럼 원본에서 언제든 다시 만들 수 있는 파생 사본. 지워도 색인기가 파일에서 복원한다(예외: AI 대화 chat_*).
용어 04
Tauri 2
Rust 껍데기 + 시스템 웹뷰로 데스크톱·모바일 앱을 만드는 프레임워크. Electron보다 가볍다. reflect-open의 껍데기가 이것이며, 같은 코드로 mac·iOS를 굴린다.
용어 05
CRDT · 결정론적 병합
CRDT = 충돌 없이 자동 병합되는 자료구조(Yjs 등). reflect-open V2는 이걸 버리고, 공유 정보로만 정렬하는 결정론적 3-way 병합으로 수렴을 만든다.
용어 06
충돌 해소 사다리(Conflict Ladder)
conflict/ladder.rs의 8단 규칙. 위 단계에서 못 풀면 아래로 내려가며, 마지막은 라벨 붙은 충돌 마커. 어느 기기서 돌려도 결과가 같다.
용어 07
읽기 전용 IPC 다리
프론트엔드가 Kysely로 만든 SQL을 db_query로 Rust에 넘겨 실행. rusqlite authorizer가 쓰기·위험 구문을 거부해 순수 읽기로 못 박는다.
용어 08
백링크(Backlink) · 지연 계산
어떤 노트를 가리키는 다른 노트들. 미리 저장하지 않고, 링크의 target_key를 SQL 뷰가 읽는 순간 조인해 계산한다 → 재색인 없이 즉시 연결.
용어 09
Kysely / rusqlite
Kysely = TS용 타입 안전 SQL 빌더(읽기 담당). rusqlite = Rust SQLite 드라이버(쓰기 담당). 같은 SQLite를 역할별로 나눠 쓴다.
용어 10
온디바이스 임베딩(fastembed · sqlite-vec)
노트를 벡터로 바꾸는 임베딩을 서버가 아니라 기기에서(fastembed+ONNX, MiniLM 384차원) 계산하고, sqlite-vecvec0 테이블에서 코사인 유사도로 검색.
용어 11
BYOK(Bring Your Own Key)
회사 서버를 거치지 않고 사용자 자신의 AI API 키로 직접 호출하는 방식. 키는 OS 키체인에 보관해 env·파일·git에 남기지 않는다.
용어 12
Meowdown
Reflect 팀이 만든 라이브 프리뷰형 마크다운 에디터(ProseMirror/ProseKit 기반). 이 저장소가 아니라 별도 저장소에 있어 에디터 내부는 여기서 안 보인다.
용어 13
그래프(Graph)
reflect-open에서 노트 폴더 하나를 부르는 이름. 하나의 그래프는 iCloud 또는 git 중 하나로만 동기화한다. 안에 daily/·notes/·.reflect/가 들어 있다.
용어 14
FTS5 · authorizer
FTS5 = SQLite 내장 전문(단어) 검색 가상 테이블. authorizer = rusqlite가 각 SQL 동작을 허용/거부 판정하는 콜백 API. reflect-open은 이걸로 IPC 통로를 읽기 전용으로 강제.

11참고 링크

원본을 직접 열어 확인하는 습관 — 특히 버전·수치는 클론한 소스 기준

저장소 · 제품
· 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

확인된 함정 모음 · 재확인 필수
읽기 전에 알아 둘 6가지

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"이라 적었지만 packageManagerpnpm@11.9.0이다.