PDF를 페이지별 배경 이미지로 깔고, 그 위에 텍스트·도형·직접 만든 객체를 레이어로 올리는 편집기와 뷰어. 프레임워크에 종속되지 않는다 — 렌더 층이 vanilla DOM 이고 React·Vue 래퍼가 같은 컴포넌트를 제공한다.
| 데모 | 편집기 · 뷰어 · 검증 케이스 · PDF 스파이크 |
| React 예제 | 설치된 dist 를 쓰는 소비자 앱 — 세로 레일 · 다크 |
| Vue 예제 | 전혀 다른 앱 구조 — 리본 툴바 · 페이지 단위 뷰어 |
두 예제는 같은 패키지로 전혀 다른 앱을 만든다 — 레이아웃·다이얼로그·뷰어 모드·툴바 전략이 모두 다르고, 둘 다 편집기·뷰어·Ports·저장·검증·문항 3종을 풀세트로 쓴다.
화면의 </> 배지를 누르면 그 부분의 코드가 나온다. 상단 [테마 ON/OFF] 는 @layer
오버라이드가 실제로 이기는지 눈으로 확인하는 장치다.
무엇을 눌러 봐야 하는지, 어떤 기능이 어느 파일에 있는지는 예제 앱 두 개 에 정리했다.
npm install @h_domi/pdf-canvas-kit아직 beta 다. prerelease 를
latest태그로 올리므로 위 명령이 최신 beta 를 받는다.
// React
import { PDFCanvasEditor } from '@h_domi/pdf-canvas-kit/react'
import '@h_domi/pdf-canvas-kit/styles.css'
<PDFCanvasEditor initialDoc={doc} onChange={setDoc} /><!-- Vue / Nuxt -->
<script setup>
import { PDFCanvasEditor } from '@h_domi/pdf-canvas-kit/vue'
import '@h_domi/pdf-canvas-kit/styles.css'
</script>
<template><PDFCanvasEditor :initial-doc="doc" @change="onChange" /></template>// 프레임워크 없이 — imperative facade
import { createPDFCanvasEditor, createPDFCanvasViewer } from '@h_domi/pdf-canvas-kit'
const editor = createPDFCanvasEditor(container, { initialDoc: doc })
const viewer = createPDFCanvasViewer(other, { doc: editor.toPublicDoc() })
initialDoc은 최초 1회만 읽는다. 편집기가 문서를 소유하고onChange로 밀어낸다 — controlled 가 아니다. 이름이 그 계약이다.
런타임 의존성은 pdfjs-dist 하나다. react · vue 는 optional peer 라 쓰는 쪽만 설치한다.
번들 크기는 코어 10.9KB + React 래퍼 3.5KB / Vue 래퍼 5.7KB + CSS 21.9KB.
패키지 전체는 140KB(압축) · 447KB(해제) 다 — 소스맵을 배포하지 않는다 (2026.08.25 실측).
이 둘을 빠뜨리면 에러 없이 조용히 잘못 동작한다.
pdf.js는 CMap·표준 폰트·wasm 을 런타임에 URL로 가져온다. 빠뜨리면 한국어 PDF에서 글자가 사라진다.
configurePdfResources({
workerSrc: '/pdfjs/pdf.worker.mjs',
cMapUrl: '/pdfjs/cmaps/', // ⚠️ 이것 없으면 한글이 사라진다
standardFontDataUrl: '/pdfjs/standard_fonts/',
wasmUrl: '/pdfjs/wasm/',
iccUrl: '/pdfjs/iccs/',
}).pck-editor 는 height: 100% 다. 확정 높이가 없으면 편집기가 접힌다.
html, body, #app { height: 100%; margin: 0; }
/* flex 안이면 min-height: 0 도 필요하다 */자세한 것은 시작하기.
전체 사용 설명서는 docs/ 에 있다.
| 예제 앱 두 개 | 처음이면 여기 — 화면 투어 · 파일 지도 · 내 앱으로 옮기기 |
| 시작하기 | 설치, pdf.js 자산, 높이·폭 함정, SSR |
| React · Vue · 프레임워크 없이 | 환경별 적용법 |
| 커스텀 객체 | PDF 위에 내 컴포넌트를 올린다 |
| 뷰어 | 읽기 전용 렌더 + 응답 받기 |
| 도형 · 글꼴 | 도형 11종 / 웹폰트는 호스트가 불러온다 |
| 저장 · 업로드 · 내보내기 | 포트 주입, 검증 게이트 |
| 스타일 오버라이드 | 토큰 80개 + @layer — 특이도 싸움이 없다 |
| 문구 · 번역 · 아이콘 | 모든 텍스트·아이콘을 교체 |
| 다이얼로그 위임 | 우리 팝업 대신 내 모달 |
| 호스트 앱에 녹이기 | 단축키 충돌 · 예외 · 이탈 확인 · 다크 모드 |
| API 레퍼런스 · 함정 모음 | prop·handle·타입 / 실제로 겪은 문제 |
| TODO | 남은 일 |
내부 구조는 따로 있다.
| ARCHITECTURE.md | 코드 구조, 무엇을 어디서 바꾸는지, 좌표계 규칙 |
| CLAUDE.md | 이 저장소의 작업 규칙 |
호스트 앱에 맞추는 길이 네 개다. 위에서부터 시도하면 대부분 첫 단계에서 끝난다.
/* 1. 토큰 — 색·간격·폭·모달 모양 */
.my-app .pck-editor {
--pck-accent: #3b82f6;
--pck-pagelist-width: 200px;
}
/* 2. CSS 규칙 — @layer 라서 단일 클래스가 이긴다. !important 불필요 */
.pck-toolbar { justify-content: center; }{/* 3. 문구·아이콘 — 번역이나 아이콘 라이브러리 */}
<PDFCanvasEditor
strings={{ 'toolbar.text': 'Text' }}
renderIcon={{ undo: UndoIcon }}
/>
{/* 4. 다이얼로그 위임 — 내 모달을 쓴다 */}
<PDFCanvasEditor
onRequestUpload={() => setMyUploadOpen(true)}
onRequestConfirm={(req) => setMyConfirm(req)}
/>{/* 5. 툴바를 내가 만든다 — 상태 스냅샷 + 명령 */}
const [handle, setHandle] = useState<EditorHandle | null>(null)
const state = useEditorState(handle)
<button disabled={!state?.canUndo} onClick={() => handle?.undo()}>되돌리기</button>
<PDFCanvasEditor ref={setHandle} />스타일 · 문구 · 아이콘 · 다이얼로그 · 내 툴바 에 각각 자세히 있다.
패키지는 기본 틀(사각형·리사이즈·색)만 그린다. 그 안을 내 컴포넌트가 채운다.
const shortAnswer = defineObjectType<Answer, Omit<Answer, 'answers'>>({
kind: 'answer.short',
label: '단답형',
defaultSize: { w: 160, h: 44 },
defaultData: () => ({ answers: [], points: 1 }),
validate: (d) => (d.answers.some((a) => a.trim()) ? null : ['정답을 입력하세요']),
toPublic: ({ answers: _a, ...rest }) => rest, // 뷰어에 나가면 안 되는 것
})<PDFCanvasEditor
objectTypes={[shortAnswer]}
renderObject={{ 'answer.short': AnswerBadge }} // 캔버스 — 미리보기
renderInspector={{ 'answer.short': AnswerFields }} // 인스펙터 — 편집
/>핸들로 틀을 키우면 안쪽 컴포넌트가 자기 CSS 대로 다시 흐른다. 커스텀 객체.
배포됨 — 아직 beta 다. npm · 데모. 편집기와 뷰어 모두 React·Vue·vanilla 에서 쓸 수 있다. 남은 일은 docs/TODO.md 에 있다.
0.1.0 (안정판) 으로 올리는 조건은 실제 앱에 붙여 보는 것이다. 지금까지 붙여 본 것은
examples/ 둘뿐이다.
| 항목 | 상태 |
|---|---|
vanilla facade (createPDFCanvasEditor) |
완료 (R9) |
React 래퍼 (@h_domi/pdf-canvas-kit/react) |
완료 (R9) — 번들 3.5KB |
Vue 래퍼 (@h_domi/pdf-canvas-kit/vue) |
완료 (R9) — 번들 5.7KB |
렌더 층 바닥 (src/dom/reactive.ts · h.ts) |
완료 (R2) |
| 객체·페이지·스테이지 렌더 | 완료 (R4·R5) |
| 편집기 크롬 (상단바·툴바·페이지목록·다이얼로그) | 완료 (R6) |
| 인스펙터 | 완료 (R7) |
| 커스텀 객체 레지스트리 | 완료 (R8) — 소비자가 타입을 정의한다 |
프레임워크 무관 컨트롤러 (src/controller/) |
완료 (R3) |
| 검증 케이스 |
432건 / 59 그룹 (npm run checks) |
| npm 배포 |
beta 배포 중 — latest 태그. tarball·registry 설치 모두 검증 (R10) |
PDFCanvasViewer |
완료 (R11) — 연속/페이지 단위 · fit width/page · renderViewer 슬롯 |
| 호스트 뷰어 크롬 | 동작 — getState() · subscribeState() · goToPage/next/prev · useViewerState
|
| 크롬 UI 슬롯 교체 | 만들지 않는다 — 대신 헤드리스 표면 (D27 · §17.5) |
| 호스트 툴바 만들기 | 동작 — getState() · subscribeState() · 명령 11개 · useEditorState
|
| 상태 | |
|---|---|
| PDF → 페이지 이미지 변환 | 동작 (페이지별 크기·회전·CropBox 확인) |
| 편집기 레이아웃 3분할 | 동작 |
| 중앙 페이지 렌더 | 동작 — 모든 페이지를 세로로 잇는다 (pageLayout="single" 로 한 장씩) |
| 좌측 페이지 리스트 · 클릭 전환 | 동작 — 클릭하면 그 페이지로 스크롤하고, 스크롤하면 하이라이트가 따라온다 |
| 확대/축소 · 팬 | 동작 (버튼 · 프리셋 · Ctrl/Cmd+휠 · Space 드래그). 그냥 휠은 세로 스크롤이다 |
| 로드 시 자동 배율 | 페이지 전체가 보이도록 맞춤 (fit-page 기본) |
| 문서 업로드 · 진행률 | 동작 (PDF만, DOC/PPT는 서버 컨버터 필요) |
| undo/redo · 타이틀 편집 | 동작 |
| 객체 생성 (텍스트·도형·커스텀) | 동작 — 도구 선택 후 드래그 |
| 객체 이동·리사이즈 (9방향 핸들) | 동작 (Shift 종횡비, Alt 중심 기준) |
| 다중 선택 (마퀴) · 복제 · 삭제 | 동작 |
| 텍스트 인라인 편집 (한글 IME 안전) | 동작 — 더블클릭 |
| 회전 (텍스트·도형) | 동작 — 회전 핸들 또는 인스펙터 |
| 지우개 | 동작 — 클릭한 객체 삭제 |
| 인스펙터 (텍스트·도형·커스텀 객체 편집) | 동작 — 커스텀 객체의 편집 창구는 여기 하나다 |
| 박스 색 편집 (배경·테두리·글자색) | 동작 — 텍스트·커스텀. 미지정은 테마 색을 따른다 |
| 검증 (내보내기 차단·실시간 경고) | 동작 — 같은 규칙을 공유 |
| 내보내기 검증 게이트 | 동작 — 실패 시 문제 객체로 이동·스크롤 |
| 내보내기 팝업 |
제공하지 않는다 — 검증 게이트만 준다 (checkBeforeExport · toPublicDoc) |
| 문항 번호 자동 부여 | 동작 — 위치에서 파생, 인스펙터에서 수동 오버라이드 |
| 패널 폭 리사이즈 | 동작 — 조정하면 localStorage에 기억 |
| 페이지 이미지 업로드 (S3) | 동작 — createS3AssetPort 또는 uploadFile prop |
| 페이지 드래그 순서 변경 | 동작 — 좌측 썸네일을 위아래로 끈다 |
| 페이지 삭제·복제 | 동작 — 썸네일 우클릭 메뉴 또는 하단 버튼. 객체가 있으면 확인 모달 |
| 자동저장 파이프라인 | 동작 — 저장 대상은 콘솔(console.debug). 실서버 미연결 |
| 상단바 [JSON 출력] | 문서 JSON 을 콘솔로. 저장이 아니라 현재 상태 보기다 |
| 상단바 [내보내기] |
EditorHandle 로 노출돼 있다 |
| 도형 | 11종 (사각·원·삼각·마름모·오각·육각·별·십자·선·화살표·양쪽화살표) |
| 글꼴 선택 | 동작 — 웹폰트 파일은 호스트가 불러온다 (글꼴) |
npm install
npm run dev # 3100 데모(레포 소스) · 3101 React 예제 · 3102 Vue 예제examples/* 는 별칭 없이 설치된 dist 를 쓴다 — exports 맵·진입점·.d.ts 를 검증하는
유일한 자리다. 패키지 소스를 고치면 npm run build 후 반영된다.
| 화면 | |
|---|---|
/editor/ |
편집기 (vanilla facade) |
/viewer/ |
편집기 ↔ 뷰어 왕복, 정답 제거 확인 |
/checks/ |
검증 케이스 397건 / 56 그룹 |
/spike/ |
PDF 변환 진단 (페이지 크기·폰트) |
| React 예제 | 테마 토글 · 호스트 모달 · 아이콘 3경로 |
| Vue 예제 | 같은 것의 SFC 판. 다른 디자인이 적용돼 있다 |
두 예제는 각 부분에 </> 배지가 있다 — 누르면 그 UI 가 어떻게 구현됐는지 코드가 나온다.
main 에 push 하면 Pages 워크플로가 세 사이트를 하나로 합쳐
배포한다 (/ · /react/ · /vue/).
| 명령 | 하는 일 |
|---|---|
npm run dev |
셋을 함께 띄운다 — 데모(:3100) + React 예제(:3101) + Vue 예제(:3102) |
npm run dev:demo |
데모만(:3100). LAN에서도 접근 가능 |
npm run dev:local |
같지만 localhost만 바인딩 |
npm run build |
라이브러리 빌드 + .d.ts 생성 |
npm run typecheck |
vue-tsc + node config 타입체크 |
npm run lint |
ESLint + Prettier 검사 |
npm run fix |
ESLint --fix + Prettier --write |
npm run checks |
검증 케이스를 브라우저 없이 실행 (397건. 실패 시 exit 1) |
npm run fixtures |
테스트 PDF 생성 (크기 혼합·회전·CropBox·100페이지·손상) |
npm run copy:pdfjs |
pdf.js 런타임 자산을 demo/public/pdfjs 로 복사 |
npm run check:docs |
문서의 죽은 상대 링크 + 예제 파일명이 실제와 맞는지 (lint 게이트에 포함) |
npm run examples:build |
예제 앱 타입체크 + 빌드. 소비 경로를 검증하는 자리 |
./publish.sh |
npm 배포. gitignore 대상. 인증은 .env 의 NPM_TOKEN(.env.example) 또는 2FA OTP. DRY_RUN=1 로 먼저 확인한다 |
npm run verify:tarball |
배포 산출물 검사 — 훅 · exports · 불필요 파일 · peer · @layer
|
npm run license-check |
의존성 라이선스 검사 (MIT/Apache-2.0/BSD/ISC만 허용) |
./publish.sh |
npm 배포. gitignore 대상. 인증은 .env 의 NPM_TOKEN(.env.example) 또는 2FA OTP. DRY_RUN=1 로 먼저 확인한다 |
npm run verify:tarball |
배포 산출물 검사 — 라이프사이클 훅 · exports 대상 · 불필요 파일 · peer 설정 |
npm run examples:build |
예제 앱 타입체크 + 빌드 (skipLibCheck: false). 소비 경로를 검증하는 자리다 |
자동 테스트 러너는 없다. 대신 npm run checks 가 커밋 전 게이트다 — 이유와 한계는
ARCHITECTURE §11.
줌·팬·스크롤·IME 는 실제 브라우저 레이아웃에 의존해 덮이지 않는다. 손으로 확인해야 한다.
자동 테스트 러너는 없다. npm run checks 가 커밋 전 게이트다 — 이유와 한계는
ARCHITECTURE §11. 줌·팬·드래그·한글 IME 는 실제 브라우저 레이아웃에 의존해
덮이지 않으므로 손으로 확인한다.
이 패키지는 MIT 다. LICENSE 참고.
MIT · Apache-2.0 · BSD · ISC · CC0 · 0BSD 만 허용한다. GPL/LGPL/AGPL/SSPL/상업 라이선스는 금지 — 이 패키지가 남의 제품에 임베드 배포되므로 copyleft 의무나 좌석 과금이 소비자에게 전염된다.
PDF 계열에 특히 함정이 많다: mupdf·iText 는 AGPL, PSPDFKit·PDFTron 은 상업 라이선스다.
런타임 의존성은 pdfjs-dist(Apache-2.0) 하나뿐이다. npm run license-check 로 검사한다.