@open-slide/core v1.16.0 · Vercel OSS Program · 2026-07-18 릴리스 기준)
당신이 /create-slide 로 "쿠버네티스 입문 5장 만들어줘"라고 말하면, 에이전트는 주제·분량·글 밀도·모션 4가지를 물어본 뒤 slides/k8s-intro/index.tsx에 페이지들을 React로 써 넣는다. open-slide는 그 파일을 자동으로 발견해 브라우저에 1920×1080 캔버스로 띄우고, 저장할 때마다 핫리로드한다.
발표할 땐 전체화면·발표자 모드(다음 장 미리보기·노트·타이머)가 준비돼 있고, 끝나면 서버 없이 열리는 정적 HTML 한 파일이나 PDF·PPTX로 내보낼 수 있다.
슬라이드 제작 도구는 크게 셋으로 나뉜다. ① PowerPoint·Keynote 같은 GUI 편집기, ② reveal.js·Slidev·Marp 같은 "마크다운/HTML로 쓰는" 개발자용 도구, 그리고 ③ open-slide가 개척하는 "에이전트가 코드로 쓰는" 도구다. open-slide의 관점은 명쾌하다 — "슬라이드는 결국 시각적인 코드다. 에이전트는 코드를 잘 쓴다. 그렇다면 사람이 슬라이드를 손으로 만들 이유가 없다."
그래서 open-slide는 "예쁜 템플릿"이 아니라 런타임(runtime)을 판다. 에이전트가 자유롭게 React를 쓸 수 있도록 캔버스 크기·스케일링·라우팅·핫리로드·발표 모드·내보내기라는 반복적이고 지루한 인프라를 대신 처리해주는 것이 이 프로젝트의 본질이다. 저장소에는 프레임워크 코어(@open-slide/core, TS/TSX 소스 164개), 스캐폴더 CLI, 데모 슬라이드 19벌, 그리고 Next.js 기반 문서 사이트가 함께 들어 있다.
.claude/skills/, AGENTS.md)를 미리 깔아두고, 편집 결과를 소스코드 파일로 주고받는다. 사람이 브라우저에서 클릭한 코멘트조차 소스 안 마커로 저장돼 에이전트에게 넘어간다.2026년 트렌딩 목록을 보면 "에이전트가 만드는 슬라이드"가 하나의 카테고리로 자리 잡았다 — open-slide 외에도 hugohe3/ppt-master(문서→네이티브 PPT), 1weiho 본인의 다른 슬라이드 실험, 각종 "슬라이드 스킬"이 동시에 떠오르고 있다. open-slide가 이 무리에서 눈에 띄는 이유는 "결과물(예쁜 PPT)"이 아니라 "제작 루프(agent loop) 자체"를 제품으로 잡았기 때문이다.
| 축 | reveal.js / Slidev / Marp | open-slide |
|---|---|---|
| 작성 방식 | 사람이 Markdown/HTML을 직접 작성 | 에이전트가 React(.tsx)를 작성, 사람은 지시·검토 |
| 페이지 표현력 | Markdown 문법 + 제한된 커스터마이즈 | 임의의 React 컴포넌트 — 문법 제약 없음 |
| 편집 루프 | 코드 수정 → 새로고침 | 화면 클릭 코멘트 → 스킬이 소스 자동 편집 |
| 에이전트 지원 | 없음 (사람이 프롬프트 관리) | 스킬 5종·설계 레퍼런스 저장소에 내장 |
| 내보내기 | HTML(주로), 일부 PDF | 정적 HTML·PDF·PPTX 전부 클라이언트에서 |
reveal.js가 "워드프로세서"라면, open-slide는 "받아쓰기 비서를 붙인 워드프로세서"다. 문서 편집기 자체(캔버스·저장·인쇄)는 둘 다 있지만, open-slide는 옆에 앉은 비서(에이전트)가 당신 말을 듣고 타이핑을 대신 해준다. 당신은 "이 제목 더 크게", "이 박스 빨간색"만 말하면 된다.
그리고 그 "말"을 비서에게 전달하는 통로가 바로 인스펙터 코멘트 마커다 — 화면에서 요소를 클릭해 메모를 남기면, 그 메모가 소스코드 안에 주석으로 심겨 비서(에이전트)가 읽어간다.
① 진짜 React라서 상한선이 없다. Markdown 기반 도구는 "표현하기 어려운 레이아웃"에서 막힌다. open-slide는 페이지가 그냥 컴포넌트라 <Steps>로 단계별 등장, MorphElement로 Keynote식 "매직 무브"(요소가 다음 장으로 이어지며 변형), 임의의 CSS 애니메이션까지 전부 가능하다.
② 사람의 손맛과 에이전트의 자동화가 한 루프에 있다. 발표 → 화면 클릭으로 코멘트 → /apply-comments → 반복. 이 왕복이 open-slide의 킬러 UX다.
③ 서버가 필요 없는 배포. 출력이 순수 정적 빌드라 Vercel·Cloudflare Pages·Netlify·Zeabur 어디든 원클릭. 발표 파일 하나를 자기완결 HTML로 공유할 수도 있다.
④ Vercel OSS Program 배지를 달고 있어 최소한의 신뢰 신호가 있다. 라이선스는 MIT로 상업적 이용도 자유롭다.
저장소는 pnpm + Turborepo 모노레포다. 하나의 저장소 안에 서로 다른 npm 패키지·앱이 여러 개 들어 있고, Turbo가 이들 사이의 빌드 순서·캐시를 관리한다.
브라우저에서 슬라이드를 그리고 편집하는 SPA다. 코어가 의존하는 주요 라이브러리:
| 레이어 | 선택 | 왜 이걸 썼나 |
|---|---|---|
| UI 프레임워크 | React 18 + react-router-dom 7 | 페이지=컴포넌트 모델. /s/:id·/presenter·/themes 등 라우팅 |
| 번들러/개발서버 | Vite 5 + @vitejs/plugin-react | 빠른 HMR·플러그인 API가 이 프로젝트의 심장(4장) |
| 스타일 | Tailwind CSS 4 + tw-animate-css | @tailwindcss/vite 플러그인으로 통합 |
| UI 컴포넌트 | shadcn + @base-ui/react | 다이얼로그·팝오버·슬라이더 등 접근성 프리미티브 |
| 드래그앤드롭 | @dnd-kit (core·sortable) | 슬라이드/폴더 순서 재배치 |
| 코드 편집(핵심) | @babel/parser + @babel/types | .tsx를 AST로 파싱해 안전하게 수정 (4장) |
| 내보내기 | html-to-image · fflate | DOM→이미지, ZIP 압축으로 자기완결 HTML/PPTX |
| 기타 | lucide-react(아이콘)·next-themes(다크모드)·sonner(토스트)·emoji-picker-react·react-image-crop | 편집 UI 부품들 |
npx @open-slide/cli init my-slide 한 줄이 하는 일. 의존성은 commander(명령 파싱)·prompts(대화형 질문)·chalk(색 출력)뿐인 가벼운 패키지다. template/ 폴더를 복사하고, 패키지 매니저를 감지해 설치하고, git 초기 커밋을 만든 뒤, 에이전트 스킬 폴더(.agents/skills)를 .claude/skills로 심볼릭 링크(윈도우면 복사)해 Claude Code가 바로 인식하게 만든다. AGENTS.md ↔ CLAUDE.md도 같은 방식으로 이어준다.
Next.js + Fumadocs로 만든 open-slide.dev. MDX로 문서를 쓰고, llms.txt·llms-full.txt 라우트를 제공해 "AI가 읽기 좋은 문서"까지 챙긴다. 랜딩 페이지 안에는 실제 슬라이드 플레이어를 임베드한다.
tsdown(라이브러리 번들·타입 생성) · Biome 2(린트+포맷, ESLint+Prettier 대체) · Vitest(테스트) · Changesets(버전·체인지로그·npm 배포 자동화) · Turbo(모노레포 태스크 오케스트레이션). 최신 감성의 "Rust/Go 기반 빠른 툴" 조합을 그대로 보여주는 좋은 표본이다.
open-slide의 진짜 설계를 이해하려면 세 가지 질문에 답하면 된다. (A) 브라우저는 slides/ 폴더의 슬라이드들을 어떻게 발견하고 핫리로드하나? (B) 사람이 화면에서 클릭한 코멘트는 어떻게 소스코드로 돌아가나? (C) 에이전트/도구는 어떻게 .tsx를 부수지 않고 수정하나?
사용자의 slides/ 폴더는 코어 앱 바깥에 있다. 브라우저 코드가 이걸 정적으로 import할 수는 없으니, open-slide는 가상 모듈(virtual module)이라는 Vite 기법을 쓴다.
virtual:open-slide/slides를 import하면, 플러그인이 slides/*/index.tsx를 glob으로 훑어 loadSlide(id) 함수와 slideIds 목록을 담은 JS 코드를 생성해 돌려준다. 슬라이드를 추가/삭제하면 이 가상 모듈만 무효화하면 되니 깔끔하다.open-slide-plugin.ts가 하는 일을 뜯어보면:
fast-glob으로 slides/*/index.{tsx,jsx,ts,js}를 찾는다. 폴더명이 슬라이드 id(SLIDE_ID_RE: 영숫자·-·_)에 안 맞으면 조용히 건너뛰고 경고를 한 번만 로깅한다(HMR마다 중복 경고 방지).export const meta에서 theme·createdAt을 정규식으로 뽑아, 파일을 실행하지 않고도 테마 칩·정렬 순서를 만든다.?t=<토큰>을 붙인다. 파일이 바뀌면 slide-changed 이벤트로 토큰을 Date.now()로 갱신 → 해당 슬라이드만 캐시를 버리고 다시 로드한다. 이것이 "현재 페이지·스크롤을 유지한 채 편집 내용만 반영"되는 비결이다.노트를 저장하면 슬라이드 파일 자체가 쓰인다. 그대로 두면 위의 slide-changed가 발동해 캔버스가 다시 그려진다. 그래서 notes-plugin이 "방금 이 파일을 노트 때문에 썼다"를 recent-writes.ts에 기록하고, handleHotUpdate에서 hasRecentWrite(file)이면 broadcast를 건너뛴다. 진짜 소스 편집은 기록되지 않으니 정상 HMR을 유지한다 — "의도한 쓰기"와 "사용자 편집"을 구별하는 작은 상태 머신이다.
발표/편집 화면에서 요소를 클릭하면 코멘트 위젯이 뜬다. "이거 빨갛게"라고 적으면, 그 메모가 소스코드 안 JSX 주석 마커로 저장된다:
{/* @slide-comment id="c-1a2b3c4d" ts="2026-07-19T..." text="<base64url(JSON)>" */}
text는 {"note":"make this red","hint":"..."}를 base64url로 인코딩한 값이다. 마커는 항상 대상 JSX 요소의 첫 자식으로 삽입된다 — 이유가 재밌다: () => ( <Foo/> )처럼 JSX 바깥에 주석을 넣으면 빈 객체 리터럴로 파싱돼 표현식이 깨지기 때문에, 반드시 컨테이너 안쪽에 넣어야 안전하다.
이 마커를 소스에 심는 쪽은 브라우저가 아니라 Vite 개발서버의 API 라우트(src/vite/routes/comments.ts)다. 즉 브라우저는 "여기에 이런 코멘트"라고 요청만 하고, 실제 파일 쓰기는 Node 쪽에서 Babel로 정확한 위치를 계산해 수행한다.
src/editing/는 이 저장소에서 가장 배울 게 많은 폴더다. 문자열 치환(replace)로 코드를 고치면 같은 텍스트가 여러 군데 있을 때 엉뚱한 곳이 바뀐다. open-slide는 대신 AST(추상 구문 트리) 기반으로 편집한다.
<div style={{color:'red'}}>를 파싱하면 "JSX요소 > 여는태그 > 속성 style > 객체 > color:red" 식의 노드 트리가 된다. Babel이 각 노드의 소스 내 시작/끝 위치(start/end)를 알려주므로, "이 노드의 이 범위만" 정확히 잘라 바꿔치기(splice)할 수 있다.edit-ops.ts가 정의하는 편집 연산(EditOp)들:
set-style — 특정 요소의 인라인 스타일 키 하나를 설정/삭제set-text / set-text-range-style — 텍스트 교체, 텍스트 일부 구간만 스타일링set-attr-asset — 이미지 src 등 속성을 에셋 경로로replace-placeholder-with-image — <ImagePlaceholder>를 실제 이미지로 치환각 연산은 { from, to, text } 형태의 Splice(소스 범위 잘라 바꾸기)로 환원된다. 여러 마커를 처리할 땐 파일 아래쪽(줄 번호 큰 것)부터 역순으로 적용한다 — 위에서부터 고치면 길이가 바뀌어 아래 마커의 줄 번호가 어긋나기 때문이다. 이 "역순 적용"은 텍스트 일괄 편집의 정석이라 그대로 훔쳐올 가치가 있다.
코어는 @open-slide/core에서 몇 가지 연출 부품을 export한다.
<Steps>/<Step> — 한 페이지 안에서 항목을 단계별로 등장시키는 "빌드" 애니메이션(PowerPoint의 "나타내기 순서").SlideTransition — 페이지 전환 시 exit/enter 키프레임을 세밀하게 정의.<MorphElement> — 인접한 두 페이지에 같은 id로 감싼 요소가 있으면, 위치·크기·모서리·색을 연속으로 보간하며 "변형"된다(Keynote의 "매직 무브"). 내부적으로 FLIP 기법(두 사각형을 측정 → 클론을 오버레이로 띄워 transform 애니메이션)으로 구현된다.모프 레퍼런스 문서에는 "실전 덱에서 하나씩 얻어낸 규칙" 7개가 있는데, 그중 useIsActivePage()로 "지금 관객이 보는 인스턴스에서만 인트로 애니메이션을 재생"하게 막는 규칙이 인상적이다. 전환 중엔 나가는 페이지·썸네일·발표자 미리보기까지 여러 인스턴스가 동시에 마운트되므로, 그것들이 애니메이션을 다시 틀면 스냅샷이 엉킨다. React의 "같은 컴포넌트가 여러 문맥에서 산다"는 현실을 정면으로 다룬 좋은 예다.
이 저장소를 처음 여는 사람에게: ① vite/open-slide-plugin.ts(어떻게 슬라이드를 찾고 핫리로드하나) → ② editing/edit-ops.ts + comments.ts(어떻게 코드를 안전하게 고치나) → ③ skills/slide-authoring/(에이전트가 실제로 뭘 읽고 쓰나) → ④ apps/demo/slides/*/index.tsx(완성된 슬라이드 실물). 이 4곳만 봐도 프로젝트의 80%가 손에 잡힌다.
resolveId/load로 존재하지 않는 모듈을 즉석 생성하고, configureServer에서 chokidar 감시자를 추가하고, handleHotUpdate로 HMR 동작을 세밀 제어하는 완결된 Vite 플러그인 실전 예제다. "빌드 도구 위에 프레임워크를 얹는" 방법을 배우기에 이보다 좋은 교재가 드물다.
정규식 치환의 위험을 AST로 넘어서는 법. 노드 위치(start/end)로 Splice를 만들고, 여러 편집을 역순 적용하고, JSX 주석을 컨테이너 첫 자식으로만 삽입해야 하는 파싱 함정까지 — 코드 생성/리팩터링 툴을 만들 사람이라면 반드시 볼 폴더다.
사람의 클릭을 소스 안 마커로 직렬화(base64url JSON)하고, 에이전트가 그걸 읽어 편집하고, 마커를 지운다. GUI의 직관성과 텍스트의 버전관리성(git diff)을 동시에 얻는 영리한 절충. AI 협업 도구를 설계할 때 참고할 만한 인터페이스 아이디어다.
cli/dev.ts는 개발 서버를 자식 프로세스로 fork하고, 앱 안에서 코어를 업데이트하면 RESTART_EXIT_CODE로 종료 → 감독 프로세스가 새 버전으로 재spawn한다. 시그널(SIGINT/SIGTERM)을 자식에 전달해 고아 프로세스를 막는 처리까지 있다. Node CLI 도구 설계의 좋은 표본.
2026년식 TS 라이브러리/모노레포를 새로 셋업할 때 그대로 베껴올 수 있는 레퍼런스 조합. changeset으로 버전·체인지로그·npm publish를 자동화하고, tsdown으로 라이브러리를 번들+타입 생성하고, biome 하나로 린트+포맷을 통일하는 흐름을 package.json scripts에서 통째로 읽을 수 있다.
export-html.ts는 페이지를 렌더한 뒤 html-to-image로 에셋 URL을 수집하고 fetch로 바이트를 받아 fflate로 묶어 자기완결 파일을 만든다. "브라우저 안에서 완결되는 문서 파이프라인"을 어떻게 짜는지 볼 수 있다.
open-slide는 로컬 개발 서버형 도구라 무거운 요구사항이 없다. LLM은 당신이 쓰는 코딩 에이전트(Claude Code 등) 쪽에서 돌아가고, open-slide 자체는 Vite 개발 서버와 브라우저만 필요하다.
| 항목 | 요구/권장 |
|---|---|
| Node.js | 18 이상 (engines.node >=18). 코어는 20/22 LTS 권장 |
| 패키지 매니저 | pnpm(권장, 저장소가 pnpm 10 사용) · npm/yarn/bun도 CLI가 감지 |
| OS | macOS·Linux·Windows. 윈도우는 심볼릭 링크 대신 복사로 폴백 |
| 브라우저 | 모던 크로미움/파이어폭스(캔버스·BroadcastChannel 발표자 채널 사용) |
| GPU | 불필요 |
| 포트 | 개발 서버 기본 5173 (Vite) |
| 배포 | 정적 호스팅(Vercel·Cloudflare Pages·Netlify·Zeabur) — 서버 런타임 없음 |
단, 실제로 "에이전트가 슬라이드를 써 주는" 경험을 온전히 누리려면 Claude Code / Codex / Cursor 같은 코딩 에이전트가 별도로 있어야 한다. open-slide 없이 손으로 slides/<id>/index.tsx를 직접 편집해도 잘 동작한다.
npx @open-slide/cli init my-slide && cd my-slide && pnpm dev로 워크스페이스를 띄운다. slides/getting-started/index.tsx를 열어 제목 텍스트를 바꿔보고, 저장 시 브라우저가 현재 페이지를 유지한 채 갱신되는지 확인한다. 발표 모드(P 등)로 들어가 발표자 뷰를 보고, 마지막에 정적 HTML로 내보내 파일 하나만으로 열리는지 검증한다.
개발 화면에서 한 요소를 클릭해 "make this bigger" 코멘트를 남긴 뒤, slides/<id>/index.tsx를 열어 @slide-comment 마커가 어느 위치(어느 JSX 요소의 첫 자식)에 심겼는지 확인한다. text 값을 base64url 디코드해 {"note":...} JSON을 눈으로 확인해보라. 인스펙터가 사람 입력을 어떻게 직렬화하는지 체감할 수 있다.
vite/open-slide-plugin.ts의 generateSlidesModule이 생성하는 코드를 console.log로 찍어보거나, 브라우저 devtools의 Sources에서 virtual:open-slide/slides를 찾아 loadSlide의 switch문과 ?t=토큰 캐시버스트를 확인한다. 슬라이드를 하나 추가/삭제하며 목록이 어떻게 바뀌는지 본다.
editing/edit-ops.ts에 새 연산(예: set-attr-number — 숫자 속성 변경)을 추가하고, AST에서 대상 속성을 찾아 Splice를 만들어 반환하도록 구현한다. edit-ops.test.ts에 케이스를 더해 vitest로 통과시켜본다. AST 편집의 핵심 감각을 얻는 과제다.
인접한 두 페이지에 같은 id의 <MorphElement>를 두고, 위치·크기·색이 다른 사각형이 부드럽게 변형되게 만든다. slide-authoring/references/morph.md의 7가지 규칙(특히 useIsActivePage() 게이팅, transform 금지)을 지키며 "한 번의 자신 있는 제스처"로 읽히게 튜닝해본다.
Vite 플러그인 API(resolveId·load·transform·configureServer·handleHotUpdate)를 공식 문서로 익히고, open-slide의 open-slide-plugin.ts를 줄 단위로 읽는다. 가상 모듈·HMR 이벤트·chokidar 감시가 목표. 곁들여 rollup 플러그인 훅과의 관계를 이해한다.
@babel/parser·@babel/types로 JS/TS를 파싱·순회·변형하는 법. editing/ 폴더를 교재 삼아 "노드 위치 기반 Splice"를 손에 익히고, jscodeshift/ts-morph 같은 코드모드 도구로 확장한다. 리팩터링 자동화·린트 룰 작성의 기초.
"같은 컴포넌트가 여러 문맥에서 마운트된다"는 현실(썸네일·발표자 미리보기·전환 스냅샷)을 다루는 법. useSyncExternalStore, FLIP 애니메이션 원리, useIsActivePage 같은 문맥 게이팅 훅 패턴. 저장소가 참조하는 vercel-react-best-practices 스킬의 규칙들도 함께 읽으면 성능 감각이 는다.
skills/*/SKILL.md가 어떻게 "에이전트가 따를 절차서"로 쓰이는지 분석한다. create-slide의 4가지 질문 설계, slide-authoring의 기술 레퍼런스 분리, apply-comments의 정확한 정규식 명세 — "프롬프트가 아니라 문서로 에이전트를 프로그래밍"하는 감각을 기른다. Claude Code 스킬/플러그인 생태계로 확장.
slides/를 발견해 loadSlide(id)를 만든다.?t=토큰으로 슬라이드별 캐시버스트를 건다.{from,to,text}로 정확히 범위만 잘라 바꾸는 편집이 Splice.text는 {note,hint}의 base64url. 대상 요소의 첫 자식으로 삽입.--osd-bg 등)로 변환돼 디자인 패널에서 라이브 조정.