플랫폼별 통합
React · Next.js
React에서는 ref로 컨테이너를 잡고 useEffect에서 마운트·정리합니다.
SDK는 npm 패키지가 아니라 전역 <script>이므로 window.Inko로 접근합니다.
공통 전제
뷰어 산출물이 /pdfv/ 경로에 배포되어 있어야 합니다 — 시작하기 참고.
React 개발 서버를 쓰는 동안은 프록시(vite.config의 server.proxy 또는 CRA proxy)로 /pdfv를 운영 서버에 연결하면 됩니다.
SDK 로드
CRA·Vite 기반 SPA는 index.html에 한 줄 추가하는 것이 가장 단순합니다.
index.html html
<!-- public/index.html (CRA·Vite) 또는 공통 레이아웃 -->
<script src="/pdfv/sdk/pdfv-sdk.js"></script>컴포넌트 예제
PdfEditor.jsx jsx
import { useEffect, useRef } from 'react';
export default function PdfEditor({ docId, pdfUrl, initialCanvasData }) {
const containerRef = useRef(null);
const viewerRef = useRef(null);
useEffect(() => {
viewerRef.current = window.Inko.mount(containerRef.current, {
src: '/pdfv/index.html',
pdfUrl,
initialCanvasData,
onSave: (canvasData, ok) => {
if (!ok) return;
fetch('/api/annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ docId, canvasData }),
});
},
});
// 언마운트 시 정리 — iframe·리스너 제거.
// React 18 StrictMode(dev)의 mount→unmount→mount 이중 실행도
// 이 cleanup 덕분에 안전합니다.
return () => viewerRef.current?.destroy();
}, [docId, pdfUrl, initialCanvasData]);
return <div ref={containerRef} style={{ width: '100%', height: '80vh' }} />;
} cleanup에서 destroy()는 필수입니다
라우트 이동·조건부 렌더로 컴포넌트가 사라질 때 viewer.destroy()를 호출하지 않으면
iframe과 message 리스너가 누적됩니다. React 18 StrictMode가 dev에서 effect를 두 번 실행하는 것도
cleanup이 있으면 문제가 되지 않습니다.
Next.js (App Router)
SSR(서버에서 HTML을 미리 그려 보내는 단계)에는 window가 없으므로 클라이언트 컴포넌트에서 사용하고, next/script의 onLoad로 SDK 로드 완료 시점을 잡습니다.
components/PdfEditor.jsx jsx
'use client'; // 클라이언트 컴포넌트 필수
import Script from 'next/script';
import { useEffect, useRef, useState } from 'react';
export default function PdfEditor({ docId, pdfUrl, initialCanvasData }) {
const containerRef = useRef(null);
const viewerRef = useRef(null);
const [sdkReady, setSdkReady] = useState(false);
// SDK 로드 완료 후 마운트
function handleSdkLoad() { setSdkReady(true); }
useEffect(() => {
if (!sdkReady) return;
viewerRef.current = window.Inko.mount(containerRef.current, {
src: '/pdfv/index.html',
pdfUrl,
initialCanvasData,
onSave: (canvasData, ok) => {
if (!ok) return;
fetch('/api/annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ docId, canvasData }),
});
},
});
return () => viewerRef.current?.destroy();
}, [sdkReady, docId, pdfUrl]);
return (
<>
<Script src="/pdfv/sdk/pdfv-sdk.js" onLoad={handleSdkLoad} />
<div ref={containerRef} style={{ width: '100%', height: '80vh' }} />
</>
);
}저장 처리 (서버 측)
onSave로 받은 canvasData를 INSERT하는 엔드포인트 예시입니다.
설계 원칙은 저장과 버전 관리를 참고하세요.
app/api/annotations/route.js js
// Next.js Route Handler 예시 — app/api/annotations/route.js
// (별도 백엔드가 있다면 그 API로 직접 전송해도 됩니다)
export async function POST(request) {
const { docId, canvasData } = await request.json();
await db.query(
'INSERT INTO doc_annotations (doc_id, canvas_data) VALUES ($1, $2)',
[docId, canvasData]
);
return Response.json({ ok: true });
}주의사항
- TypeScript — 전역 타입이 필요하면
declare global { interface Window { Inko: any } }선언을 추가하세요. - 의존성 배열 —
docId·pdfUrl이 바뀔 때 재마운트하는 구조입니다. 재마운트 없이 문서만 바꾸려면 effect 분리 후viewer.loadPdfUrl()을 호출하세요. - 상태로 두지 마세요 — viewer 인스턴스는 렌더와 무관하므로
useState가 아닌useRef에 보관합니다.