지금 이 글이 올라가 있는 블로그도 Quartz로 만들어졌다. Quartz는 Obsidian 노트를 웹으로 서빙하는 훌륭한 도구지만 UI가 마음에 들지 않았다. shadcn/ui 기반으로 다시 만들고, 장기적으로는 AI Agent까지 붙이고 싶었다. 그게 nuartz의 출발점이다. 처음엔 Quartz 코드를 복사해서 래핑하는 전략을 썼다가 구조적 문제를 발견했고, 플러그인을 해부하면서 실제로 재사용 가능한 것과 그렇지 않은 것을 구분하게 됐다. 이 글은 그 과정에서 고민한 것들의 기록이다.
아이러니
이 글 자체는 Quartz 위에서 작성되고 서빙된다. nuartz가 완성되면 이 글도 그 위에서 보이게 될 것이다.
목적
지금 운영 중인 블로그는 Quartz를 기반으로 한다. Quartz는 Obsidian 볼트를 그대로 웹사이트로 변환해주는 정적 사이트 생성기로, wikilink, callout, backlink, graph view까지 지원하는 완성도 높은 도구다.
그런데 쓰다 보니 두 가지가 계속 걸렸다.
첫째, UI가 마음에 안 든다. Quartz의 기본 디자인은 나쁘지 않지만, shadcn/ui 기반으로 직접 만들고 싶었다. 커스터마이징 여지도 생기고, React 생태계의 컴포넌트를 자유롭게 쓸 수 있으니까.
둘째, AI Agent를 붙이고 싶다. 단순히 글을 보여주는 것을 넘어서, 내 Obsidian 노트를 기반으로 질문에 답하고, 노트 간 연결을 탐색하고, 파일 기반 검색이 가능한 시스템을 만들고 싶었다. 결국 지식 관리 도구로 확장하고 싶었다.
그래서 목표는 이렇게 정리됐다:
Obsidian으로 노트 관리
+
Next.js + shadcn/ui로 웹 서빙
+
LangGraph + RAG으로 AI Agent 채팅
이 프로젝트 이름을 nuartz라고 붙였다. Next.js + Quartz.
첫 번째 접근: sync-quartz
아이디어
처음 떠올린 전략은 단순했다. Quartz를 그대로 쓰되, Next.js 위에서 돌아가도록 래핑하자.
upstream/quartz/ (Quartz 소스코드 clone)
↓ sync-quartz.ts 스크립트로 복사
packages/nuartz/quartz/ (복사된 Quartz 플러그인)
↓ nuartz 코드가 래핑
packages/nuartz/src/ (Next.js 통합 레이어)
sync-quartz.ts 스크립트가 Quartz 레포를 clone하고, 필요한 파일들을 복사해오는 방식이다. Quartz가 업데이트되면 스크립트를 다시 돌리면 된다는 아이디어였다.
// scripts/sync-quartz.tsconst SYNC_TARGETS = [ { from: 'quartz/plugins', to: 'plugins' }, { from: 'quartz/util', to: 'util' }, { from: 'quartz/components', to: 'components' }, // ...]// upstream Quartz에서 파일 복사await cp(sourcePath, destPath, { recursive: true })
혼자 유지보수할 시간이 없으니, Quartz 개발자들이 업데이트하는 걸 가져오는 식으로 하자는 생각이었다.
왜 문제였나
실제로 만들어보니 구조적인 문제가 있었다.
"auto-sync"의 실체
sync-quartz 전략은 결국 파일 복사다.
- 진짜 auto-sync: 버전 의존성으로 관리+ 실제 동작: Quartz 소스코드를 cp로 복사
Quartz 내부 구조가 바뀌면 복사해도 타입 에러, 런타임 에러가 난다. 결국 Quartz 내부 변화를 직접 추적해야 한다. 포크를 유지보수하는 것과 난이도가 비슷하다.
게다가 실제로 구현을 시작하자마자 막혔다. Quartz의 transformer 플러그인들이 inline script를 emit하는데, 이 스크립트들이 Quartz 자체 런타임을 가정하고 있어서 Next.js에서 그대로 쓸 수가 없었다.
결국 transformer wrapper는 전부 빈 배열을 반환하는 상태로 멈췄다:
// packages/nuartz/src/plugins/transformers/index.tsexport function getDefaultTransformers(): QuartzTransformerPluginInstance[] { // TODO: inline script 이슈 해결 후 구현 return [] // ← 아무것도 안 함}
README에는 ”✅ Full Obsidian Compatibility”라고 써있지만, wikilink도 callout도 아무것도 처리하지 않는 상태였다.
ctx.allSlugs는 broken wikilink 감지 옵션에서만 쓰이고, 나머지는 ctx 의존성이 거의 없다. 빈 배열로 넘겨도 동작한다.
sync-quartz는 이 목적으로는 유효하다
OFM 파일 하나만 복사해서 쓰는 용도라면 sync-quartz 전략은 여전히 의미가 있다. 문제는 Quartz 전체를 래핑하려 했던 초기 설계였다.
수정된 전략:
upstream/quartz/quartz/plugins/transformers/ofm.ts ← 이것만 가져옴
나머지는 npm 패키지 직접 사용
AI 스택 선택
Next.js + shadcn/ui로 웹 서빙을 하고 나면 두 번째 목표인 AI Agent를 붙여야 한다.
Next.js가 맞는 이유
AI Agent 기능이 들어가는 순간 서버가 필요하다.
사용자 질문 → API Route → LLM 호출 → 스트리밍 응답
Quartz, Astro(static), GitHub Pages로는 불가능하다. Next.js의 API Routes / Server Actions가 자연스러운 선택이다.
Vercel AI SDK vs LangGraph
처음엔 Vercel AI SDK를 고려했다. Next.js와 통합이 제일 깔끔하고, streaming UI도 useChat 훅 하나로 된다.
그런데 실제 docs를 보니 “Skills/Harness” 개념이 다르다는 걸 알았다.
Vercel AI SDK의 "Skills"는 다른 개념
Vercel Skills = SKILL.md 파일 기반 “플러그인 프롬프트”
코드 에이전트(Cursor 같은 것)에 명령 파일을 로드하는 개념이다. 내가 원하는 graph-based skill routing이 아니다.
Vercel AI SDK의 실제 한계 (공식 docs 명시):
체크포인팅 없음 → 직접 구현해야 함
Human-in-the-loop 없음
문서에서 직접 “비결정적(non-deterministic)으로 설계됐다”고 인정
내가 원하는 harness 패턴은 LangGraph의 개념이다.
LangGraph JS vs Python
LangGraph JS
LangGraph Python
성숙도
v1.0.x, production 가능
레퍼런스 구현, 가장 성숙
Next.js 통합
동일 코드베이스
별도 Python 서버 필요
체크포인팅
PostgreSQL adapter 있음
AsyncPostgresSaver (battle-tested)
deepagents harness
없음
✅ 있음
ML 생태계
보통
압도적
새 기능
Python 이후 도착
먼저 나옴
내가 내린 결론
AI 개발자로서 제대로 만들려면 LangGraph Python + deepagents harness가 맞다.
deepagents는 LangChain이 만든 agent harness 프레임워크로, planning → task decomposition → subagent spawn → skill routing이 전부 구현되어 있다. Python이 가진 ML 생태계(벡터 DB, 문서 로더, 임베딩)와 함께 쓰면 RAG 파이프라인도 훨씬 풍부하다.
Next.js와의 통합은 기존 portfolio-ai 패턴처럼 Python 서버를 별도로 띄우고 API로 호출하면 된다.
nuartz (Next.js)
└── app/api/chat/route.ts ← Python 서버 호출
portfolio-ai (LangGraph Python)
├── skills/
│ ├── rag_search.py ← Obsidian 노트 검색
│ ├── graph_traverse.py ← 노트 간 연결 탐색
│ └── summarize.py ← 요약
└── agent/
└── nuartz_agent.py ← deepagents harness
Next.js 구조는 건드리지 않고 /api/chat 라우트 하나만 추가하면 된다.
마치며
nuartz를 기획하면서 가장 크게 깨달은 것은, Quartz는 라이브러리가 아니라는 것이다.
Quartz는 완성도 높은 정적 사이트 생성기다. 내부를 뜯어서 다른 프레임워크에 끼워 맞추려는 시도는 처음부터 설계 불일치를 안고 가는 것이었다.
반면 OFM 플러그인 하나는 진짜 재사용 가능한 자산이다. wikilink와 callout 파싱 로직은 Quartz 개발자들이 공들여 만든 것이고, markdownPlugins / htmlPlugins 인터페이스 덕분에 unified 파이프라인에 깔끔하게 꽂을 수 있다.
결국 “Quartz를 통째로 가져오려는 욕심”을 내려놓고 “필요한 것만 가져오는 현실적인 전략”으로 방향을 바꿨다. 나머지는 npm 생태계가 이미 잘 해결해두었다.