노트는 그냥 마크다운 파일 하나로 충분하다. 빌드 시스템 없는 PWA + Go 서버 + 텔레그램 봇으로 만든 안티-Second-Brain 선언문.
files.md를 이해하려면 PKM이라는 동네부터.
2020년대 들어 Obsidian·Roam·Notion·Logseq 같은 도구가 폭발적으로 늘었다. 공통 명칭은 PKM (Personal Knowledge Management). "내 머릿속 지식을 외부 시스템에 차곡차곡 쌓아두자"는 사상이다.
그런데 이 PKM 도구들에는 공통된 문제가 있다. 그래프 뷰, 백링크, 플러그인, 동기화 서비스, 자체 포맷… 기능이 늘어날수록 내 머리는 점점 비워진다. 정작 노트는 쌓이지만 다시 안 읽는다. files.md는 이 흐름을 정면으로 비판하며 등장했다.
한 문장으로 줄이면.
files.md는 코드량보다 철학이 먼저다. 모든 데이터는 .md 파일과 폴더. 빌드 시스템 없음. 플러그인 없음. 의존성은 vendor/에 직접 포함. 입력 채널은 웹앱 + 텔레그램 봇 두 가지. 동기화는 mtime 기반 + append-only 로그.
Obsidian이 "스위스 아미 나이프"라면, files.md는 "잘 갈린 부엌칼 한 자루". 기능을 뺀 만큼 머리는 비워진다는 게 저자 zakirullin의 5년짜리 실제 시스템이다.
시대적 배경 + 경쟁 우위 + 저자 후광.
| 비교 항목 | Obsidian · Notion · Roam | files.md |
|---|---|---|
| 설치 | 네이티브 앱 다운로드 | 브라우저에서 PWA로 설치 (혹은 그냥 URL 열기) |
| 저장 방식 | 자체 포맷 + 마크다운 + SQLite | 오직 .md 파일과 폴더 |
| 플러그인 | 수백 개의 커뮤니티 플러그인 | 없음. 의도적으로 차단 |
| 빌드 시스템 | Webpack/Vite/Esbuild | 없음. web/index.html이 그냥 동작 |
| 의존성 | node_modules 수백 MB | vendor/에 모든 의존성 포함 |
| 입력 채널 | 앱 자체 | 웹앱 + Telegram 봇 (모바일 메모) |
| 동기화 | 유료 서비스 또는 iCloud | 자체 Go 서버 또는 iCloud/Dropbox 폴더 |
Obsidian이 다기능 스위스 아미 나이프라면, files.md는 잘 갈린 부엌칼 한 자루다. 기능을 뺀 만큼 머리는 비워지고, 10년 뒤에도 그대로 굴러간다.
저자 zakirullin은 README에서 PKM 커뮤니티가 빠지기 쉬운 함정을 정면으로 비판한다. 영문 원문 일부를 풀어 옮기면:
"PKM 도구를 쓰기 시작했을 때, 나는 망각의 문제를 풀고 있다고 믿었다. 나중에는 통합의 문제를 풀고 있다고 생각했다. 결국 깨달은 건, 내가 만든 게 새로운 문제 — 미루기(deferral) 였다는 사실이다. 시스템이 커질수록, 나는 정리·태깅·증류·금 캐내기 작업을 미래의 어떤 나에게로 미루고 있었다. 그 미래의 나는 절대 오지 않았다."
그래서 files.md의 결론은 "노트는 적게, 생각은 깊이"다. 폴더 구조는 처음엔 0개로 시작하고, 노트 하나에 아이디어 하나만, 새로운 지식은 미루지 말고 즉시 적용. 도구가 똑똑해질수록 내 뇌는 멍청해진다는 경고가 핵심이다.
.md라 ChatGPT/Claude에 그대로 던지면 컨텍스트가 된다. files.md/llms.txt까지 제공한다.백엔드 · 프론트엔드 · 인프라.
.env 파일 로드 (BOT_API_TOKEN 등)go.mod만 있어도 빌드되지만, vendor/ 폴더를 두면 외부 네트워크 없이도 빌드된다. 저자는 "삭제된 의존성 걱정 없도록 자급자족이 우선"이라고 말한다.PATCHED 키워드로 수정 부분 표시DirHandle 저장 → 재방문 시 동일 폴더 자동 재연결server.files 스냅샷, lastServerOk 인증 표식web/index.html만 열면 그대로 동작웹 프레임워크 없이 vanilla JS만 쓰는 건, 자전거에 변속기 없이 평지만 달리겠다는 결심과 비슷하다. 단순한 만큼 부품이 적고, 10년 뒤에도 그대로 굴러간다. "10년 뒤에도 /web/index.html을 열면 그냥 동작해야 한다" — 저자가 README에 못 박은 룰.
make init_server + make deploy_systemd. Debian 계열 호스트에 systemd 서비스0 0 * * * cd /app/storage/<USER_ID> && git add . && git commit -m "..."journalctl -u filesmd로 로그 확인. 별도 모니터링 스택 없음전체 구조도 → 핵심 패턴 하나하나.
┌───────────────────────────────────────────────────────────────────────┐
│ 사용자 디바이스 │
│ │
│ ┌──────────────────┐ ┌─────────────────────┐ │
│ │ PWA (브라우저) │ ◀──────▶│ 로컬 .md 파일들 │ │
│ │ - CodeMirror │ OPFS/ │ - brain/*.md │ │
│ │ - Vanilla JS │ FSAPI │ - journal/*.md │ │
│ │ - localStorage │ │ - habits/*.md │ │
│ └────────┬─────────┘ └─────────────────────┘ │
│ │ HTTPS │
└────────────┼──────────────────────────────────────────────────────────┘
│
│ POST /syncFile (단일 파일 동기화)
│ POST /syncFilenames (배치 동기화)
│ POST /syncMediaFile (이미지 업로드)
▼
┌───────────────────────────────────────────────────────────────────────┐
│ 서버 (단일 Go 바이너리) │
│ │
│ ┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ sync API │ │ Telegram Bot │ │ Cron Worker │ │
│ │ (server/sync) │ │ (server/bot.go)│ │ (robfig/cron) │ │
│ └───────┬────────┘ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └───────────┬───────────┴───────────┬───────────┘ │
│ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ 파일 시스템 (afero abstraction) │ │
│ │ /app/storage/<USER_ID>/*.md │ │
│ │ + tokens/ + fslog (renames 로그) │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
│
│ Pull API (long poll)
▼
┌───────────────────────┐
│ Telegram (외부) │
│ 사용자가 메모 전송 │
└───────────────────────┘
일반 웹앱은 "서버가 진실의 원천(source of truth)"이다. files.md는 정반대로 "로컬 디스크가 진실"이다. 서버가 없어도 앱은 동작한다. 사용자가 폴더를 열어주면 그 자리에서 .md 파일이 쓰여진다. 서버를 붙이면 여러 디바이스 간 동기화가 추가될 뿐이다.
로컬 우선이지만 동기화는 만만치 않다. 같은 파일이 두 디바이스에서 동시에 수정되면? files.md의 답은 의외로 단순하다 — "timestamp 두 개로 모든 걸 판단한다".
| 타임스탬프 | 의미 | 용도 |
|---|---|---|
mtime | 파일 내용 마지막 수정 시간 | 내용 동기화 비교 |
ctime | 파일 메타데이터(이동/이름변경) 변경 시간 | 이름변경/이동 추적 |
lastClientSynced | 클라이언트가 마지막으로 서버 반영을 확인한 시점 | "여기까지 봤어요" 포인터 |
동기화는 두 가지 경로로 작동한다:
POST /syncFilenames): 수정된 파일 목록 + 삭제된 파일 목록 + 디렉토리별 포인터를 보내고, 서버는 그 포인터보다 최신인 파일들을 응답.POST /syncFile): 현재 에디터로 열려있는 파일 하나만 따로. 활발히 편집 중인 파일은 배치에서 제외하고 별도 채널로 처리해 경쟁 상태(race condition)를 회피.Dropbox 같은 클라우드 폴더에서는 ctime이 임의로 바뀐다. 권한이나 소유자가 바뀌어도 ctime이 갱신되기 때문에 "진짜 내용이 바뀐 것"을 가려내기 어렵다. 그래서 내용 동기화는 mtime, 이름변경은 append-only 로그(fslog)로 분리했다.
파일 내용 동기화는 stateless로 처리되지만, 파일 이름변경과 삭제는 그렇게 못 한다. 클라이언트가 "이 파일이 사라졌네"라고 판단할 때, 그게 "삭제된 것"인지 "이름 바뀐 것"인지 알 수 없기 때문이다.
그래서 files.md는 서버에 fslog라는 append-only 로그를 둔다. 이름변경/삭제 이벤트가 모두 여기 쌓이고, 클라이언트는 자기가 마지막으로 읽은 위치 다음부터 읽어 동기화한다.
은행이 잔액을 직접 저장하지 않고 "거래 기록"만 쌓은 뒤 합산해서 잔액을 구하는 것과 같다. 상태(state)보다 이벤트(event)가 진실에 가깝다. 카프카, 데이터베이스 WAL, 블록체인 모두 같은 패턴이다.
저자의 통찰: "PKM 도구의 가장 큰 적은 '입력 마찰'". 모바일에서 앱을 켜고, 폴더 정하고, 파일명 짓고... 이 과정에서 80%의 메모는 사라진다. 그래서 files.md는 텔레그램을 입력 채널로 활용한다.
Chat.md에 저장됨brain/, journal/ 등으로 이동봇은 외부에 포트를 열지 않는다. Telegram의 long-poll API를 사용하기 때문에 NAT 안쪽 라즈베리파이에서도 돌릴 수 있다.
README ADR 2025-06-30 — 저자 본인의 결정. "회의 중에 머릿속에서 나오는 모든 걸 그냥 한 입력 필드에 떨어뜨리고 싶었다. 나중에 분류하면 된다." 입력 마찰을 0으로 만드는 게 핵심.
어디에 뭐가 있는지.
files.md/ ├── cmd/ # 명령행 도구들 (Go의 관습) │ ├── server/ # 메인 서버 entrypoint │ ├── whoop/ # 웨어러블(Whoop) 데이터를 일기에 자동 추가 │ ├── tomdlinks/ # [[wikilinks]] → [표준 마크다운 링크] 변환 │ ├── backlink/ # 역참조 링크 자동 삽입 │ └── shifttime/ # 일기 타임스탬프 시간대 이동 │ ├── server/ # 서버 비즈니스 로직 │ ├── bot.go # 텔레그램 봇 핸들러 │ └── sync/ # 동기화 API 핵심 │ ├── web/ # PWA 프론트엔드 │ ├── index.html # 단일 진입점 (빌드 불필요) │ ├── lib/ # 프론트엔드 라이브러리들 (PATCHED 키워드 표시) │ └── img/ # 스크린샷, 아이콘 │ ├── tests/ # E2E 테스트 (앱 + 서버 동시에) ├── vendor/ # 벤더된 Go 의존성 (외부 네트워크 불필요) ├── docs/ # 아키텍처 문서 (Mermaid 다이어그램 포함) │ ├── sync-flow.md # 동기화 흐름 상세 │ ├── your-own-server.md# 셀프호스팅 가이드 │ └── bot.md # 봇 사용법 │ ├── Makefile # init_server / deploy_systemd 명령 ├── go.mod / go.sum # Go 모듈 정의 ├── LICENSE # MIT └── README.md # 철학 + 사용법 + ADR(!) 일체형
저자는 마크다운 폴더를 마치 유닉스 파일 시스템처럼 다룬다. 작은 Go 프로그램들이 각자 한 가지 일만 한다.
# 위키링크를 표준 마크다운 링크로 변환 go run cmd/tomdlinks/tomdlinks.go . # Whoop(웨어러블)에서 어제의 수면/심박수 가져와 # journal/2026.05 May.md에 추가 go run cmd/whoop/whoop.go # 모든 파일에 "이 파일을 참조하는 다른 파일들" 섹션 자동 삽입 go run cmd/backlink/backlink.go
"한 가지 일을 잘하는 작은 도구를 조합하라" — 유닉스 철학의 마크다운 버전이다. ls | grep | sort처럼 tomdlinks | backlink | shifttime을 조합한다.
대부분의 레포는 README에 사용법만 적는다. files.md README의 절반은 ADR (Architecture Decision Records)로 채워져 있다. "왜 이걸 이렇게 만들었나"의 시간순 기록이다.
2024-10-06 — fyne.io를 제거했다. Electron의 대안으로 좋아보였지만, 스크롤·이모지·텍스트 선택 같은 디테일을 구현하는 데 엄청난 노력이 필요했다. 결국 웹 스택을 쓰기로. ("데스크탑에 웹 스택 쓰는 거 싫어하지만 다른 선택지가 없어보인다")
2025-09-21 — WASM 제거했다. 메시지 이동 시 버그가 났는데 재현 못 했다. JS→Go→JS 콜백 체인이 너무 복잡해서 디버깅이 불가능했다. inbox.wasm 파일이 8MB라는 것도 문제였다. JS로 재구현했다.
2025-06-30 — 모든 흐름을 Chat.md로 통합 결정. 회의 중에 머릿속에서 나오는 모든 걸 그냥 한 입력 필드에 떨어뜨리고 싶었다. 나중에 분류하면 된다.
이 ADR 섹션은 단순한 기록이 아니다. "우리가 이 길에 도달하기까지 어떤 길을 거쳤는가"의 박물관이다. 코드를 읽기 전에 ADR을 읽으면 "왜 이 함수가 이렇게 생겼지?"가 풀린다.
코드 스타일이 아니라 인생 철학에 가깝다.
README의 "How to contribute" 섹션은 다른 어떤 레포에서도 보기 힘든 강한 의견을 담고 있다.
1. 주니어 개발자가 이해할 수 있어야 한다.
2. 이상적으로, 모든 PR은 코드를 추가가 아니라 제거하거나 단순화해야 한다.
3. 코드가 적을수록 우리는 유연해진다.
4. 모든 의존성은 우리의 책임이다. 가능하면 의존성을 피하라.
5. 코드는 자급자족해야 한다 (vendor/, web/lib/가 레포에 포함된 이유).
6. "이 기능이 정말 필요한가? 진짜 일을 돕는가, 아니면 도파민만 주는가?"
get* 접두사 사용 안 함 (예: GetUser() 대신 User())PATCHED 키워드로 표시/web/index.html을 열면 그냥 동작해야 한다"기술 영역마다 무엇을 가져갈지.
cmd/에 진입점, server/에 비즈니스 로직, vendor/에 의존성spf13/afero로 파일시스템 추상화 → 테스트 가능성 확보실습: cmd/에 자기만의 작은 유틸리티(예: 오래된 일기 자동 아카이브) 추가해보기. afero로 인메모리 파일시스템 테스트.
showDirectoryPicker()로 얻은 FileSystemDirectoryHandle을 IndexedDB에 보관실습: 빈 HTML 파일 하나로 시작해서 "로컬 폴더의 마크다운 파일 목록 보여주는 PWA" 200줄로 완성.
Inline Mode)로 검색 기능 구현실습: 텔레그램에서 받은 메시지를 자기 노트 폴더의 inbox.md에 자동 append하는 미니 봇.
lastServerOk) 패턴실습: "두 폴더를 자동 동기화하는 미니 Dropbox"를 Go로 구현.
실습: 자기 사이드 프로젝트에 ADR 섹션 추가하고 결정마다 한 줄씩 적기.
모드별 최소 사양.
| 모드 | 요구사항 | 외부 노출 |
|---|---|---|
| 브라우저만 사용 | Chromium 계열 (File System Access API 지원) | 불필요 — OPFS만으로 OK |
| Safari/Firefox | OPFS만 사용 (실제 폴더 미지원) | 불필요 |
| 오프라인 | 서비스 워커가 캐싱 → 네트워크 없어도 동작 | 불필요 |
| 셀프호스팅 서버 | Debian/Ubuntu + Go 1.24+, SSH 가능 호스트 | HTTPS 포트 노출 |
| 라즈베리파이 | 가능. 매우 가벼움 | 봇만 쓰면 노출 불필요 |
| 봇 전용 | 로컬 PC도 OK (Long-poll API) | 전혀 불필요 |
BOT_API_TOKEN만 .env에 설정하고 make init_server + make deploy_systemd. storage/<USER_ID> 폴더가 자동 생성되고 텔레그램 봇이 입력 채널 역할을 한다. 외부 포트 노출이 부담스러우면 봇 모드만 써도 충분.
난이도별 실습 — Easy → Hard.
1) Chrome에서 app.files.md 접속 2) 로컬 폴더 하나 열기 3) brain/첫노트.md 만들고 두 번째 노트와 [키로 연결 4) Finder/탐색기에서 직접 열어 .md 파일이 정말로 거기에 있는지 확인
배우는 것: File System Access API의 실체, "정말 내 디스크에 쓰는구나"의 감각
1) @FilesMDBot에서 메시지 하나 보내기 2) 웹앱에서 Chat.md를 새로고침해서 동기화 시간 측정 3) 모바일 잠금 화면에서 "노트 한 줄" 떠올렸을 때 텔레그램만 켜서 보내는 워크플로우 체험
배우는 것: "입력 마찰 0" 체감 — 왜 저자가 Chat.md로 모든 흐름을 통합했는지 이해
1) Go 설치 2) cmd/wordcount/wordcount.go 새로 생성 3) 현재 폴더의 모든 .md 파일 단어 수 합산 4) 가장 긴 파일 Top 5 출력 5) 기존 cmd/backlink/backlink.go 패턴 참고
배우는 것: Go 표준 라이브러리만으로 파일 처리, "유닉스 도구 한 개 추가" 패턴
1) 로컬 폴더의 모든 .md를 합쳐 OpenAI/Anthropic API로 전송 2) "최근 일주일 일기에서 반복되는 키워드 3개" 추출 3) Python 스크립트로 자동화 4) 결과를 brain/keywords.md로 저장
배우는 것: files.md의 "LLM-friendly" 주장을 직접 검증, llms.txt의 효용 체감
1) 저렴한 VPS 또는 라즈베리파이에 make init_server로 서버 배포 2) 데스크탑과 휴대폰에서 동시에 같은 파일 편집 3) 충돌 동작 관찰 4) POST /syncFilenames 응답을 브라우저 Network 탭에서 캡처 5) README의 sync-flow.md와 매칭
배우는 것: 실전 동기화 알고리즘, mtime/ctime/lastClientSynced 3개가 만드는 정합성
주차별로 따라가면 local-first PKM 마스터.
Ink & Switch "Local-first software" 논문 읽기 → CRDT 개념 입문 → files.md README의 ADR 섹션 처음부터 끝까지 정독
MDN의 File System Access API 문서 → navigator.storage.getDirectory()로 OPFS 다루기 → FileSystemDirectoryHandle을 IndexedDB에 저장/복원 → "브라우저 안에서만 동작하는 노트 앱" 200줄로 완성
표준 라이브러리 net/http만으로 REST API 구축 → spf13/afero로 파일시스템 추상화 → 벤더링 (go mod vendor) → systemd 서비스 파일과 journalctl
두 클라이언트 동시 편집 충돌 케이스 정리 → mtime 기반 비교 vs Hash 비교 trade-off → append-only 로그 + 클라이언트 포인터 관리 → "두 폴더를 자동 동기화하는 미니 Dropbox" Go로 구현
go-telegram-bot-api로 봇 만들기 → Inline mode와 callback button 처리 → cron으로 "매일 아침 일기 템플릿 자동 생성" 워커 추가 → 웨어러블 API(Whoop, Oura, Garmin) 데이터를 일기에 자동 주입
저자의 cognitive-load 레포 정독 → Joan Westenberg의 "I Deleted My Second Brain" 에세이 읽기 → 본인 노트 폴더에서 1년 이상 안 본 파일 비율 측정 → 도구를 다이어트할지, 사용 패턴을 바꿀지 결정
자주 마주칠 단어들.
vendor/ 폴더가 대표적. 빌드 재현성과 자급자족.systemctl start filesmd / journalctl -u filesmd로 서비스·로그 관리.공식 · 사상 · 학습 리소스.
.md 파일 확인. "정말 내 디스크에 쓰는구나"를 손으로 느낀다.Chat.md 새로고침. "입력 마찰 0"이 어떤 느낌인지 직접 느껴봐야 저자의 의도가 이해된다.cmd/wordcount/wordcount.go 같은 작은 유틸리티 한 개를 직접 작성. 유닉스 철학의 마크다운 버전이 어떤 느낌인지 손으로 체득.