ryong.logryong.log
📄
2026년 3월 31일·읽는 데 약 2분

PSPDFKit에서 pdfjs-dist로: 유료 PDF 라이브러리 교체 삽질기

관련 글

OpenClaw를 활용한 AI Agent Orchestration 실험

2026. 3. 23.

OpenClaw를 활용한 hotfix 대응구축팀 설계 및 문서 관리 체계

2026. 3. 6.

BottomSheet Drag Close Handler 적용 가이드

2026. 2. 27.

교육 플랫폼 TOPIA Live에서 학생들이 PDF 교재 위에 펜으로 쓰고, 텍스트를 넣고, 지우개로 지우는 기능이 있다. 기존에는 PSPDFKit이라는 상용 라이브러리를 써왔는데, 연간 라이선스 비용이 만만치 않다. 그래서 오픈소스 pdfjs-dist 기반으로 자체 PDF 에디터를 만들어보기로 했다.

이 글은 그 과정에서 만난 문제들과, 결국 왜 PSPDFKit을 당분간 유지하기로 했는지에 대한 기록이다.

1. 왜 교체하려 했나

PSPDFKit은 잘 만들어진 라이브러리다. 펜, 지우개, 텍스트, undo/redo까지 다 있고, 무엇보다 어노테이션을 PDF에 저장하고 다시 불러와도 편집이 가능하다. 문제는 비용이다. 연간 수백 달러의 라이선스비, 그리고 번들 사이즈만 40MB+. 우리 서비스 규모에서 이게 과연 합리적인가 하는 의문이 있었다.

대안은 Mozilla의 pdfjs-dist. 무료, 오픈소스, 번들도 약 2MB. 다만 PDF 편집 기능(annotation editor)은 비교적 최근에 추가된 실험적 기능이다.

2. 만들었던 것들

디자이너가 /labs/pdf-editor에서 프로토타입을 만들었다. UI는 훌륭했다. 이걸 공통 컴포넌트로 분리하고 실제 서비스에 적용하는 작업을 진행했다.

컴포넌트 리팩토링: 3,437줄짜리 단일 파일을 12개 파일로 분리했다. 펜 엔진, 지우개 엔진, 텍스트 에디터, 선택/리사이즈 엔진, 툴바 UI를 각각의 커스텀 훅과 컴포넌트로 추출. PdfEditor.tsx는 이들을 조립하는 260줄짜리 파일이 됐다.

추가 기능도 붙였다. 페이지 썸네일 사이드바(canvas로 실제 페이지를 렌더링), 툴바 최소화 애니메이션(framer-motion), 보기 전용 모드 토글. TS 5.7+ Uint8Array 제네릭 호환 이슈도 잡았다.

3. 부딪힌 벽: 어노테이션 영속성

실제 서비스 페이지(/progress-homework/pdf)에 적용하고 테스트하던 중 치명적인 문제를 발견했다. 학생이 PDF 위에 그림을 그리고 임시저장한 뒤 새로고침하면, 그 그림이 PDF 이미지에 굽혀져서(flatten) 더 이상 수정하거나 지울 수 없었다.

PSPDFKit에서는 같은 시나리오에서 어노테이션이 여전히 편집 가능하다. 왜?

4. PSPDFKit vs pdfjs-dist: 근본적인 차이

PSPDFKit의 exportPDF()는 어노테이션을 ISO 32000 표준의 /Annot 객체로 PDF 바이너리에 기록한다. 이건 PDF 스펙에 정의된 표준 형식이라, Adobe Reader에서도, macOS Preview에서도 보이고, PSPDFKit에서 다시 열면 편집도 된다. PDF 파일 자체가 '이 위치에 이런 잉크 스트로크가 있다'는 메타데이터를 갖고 있는 거다.

pdfjs-dist는 다르다. 어노테이션을 JavaScript 메모리의 HTML 오버레이로 관리한다. saveDocument()를 호출하면 PDF에 기록은 하지만, 다시 로드했을 때 그걸 편집 가능한 에디터 객체로 복원하지 못한다. pdfjs 내부에 두 개의 시스템이 있기 때문이다:

  • Annotation renderer: 기존 PDF 안의 /Annot 객체를 읽어서 화면에 그리는 시스템 (읽기 전용)
  • Annotation editor: 사용자가 새로 그리는 잉크/텍스트를 관리하는 시스템 (편집 가능)

saveDocument()로 저장한 어노테이션은 renderer 시스템으로 로드된다. editor 시스템으로 안 올라온다. 그래서 보이긴 하는데 수정이 안 되는 거다. 이 'round-trip' 기능이 PSPDFKit이 유료인 핵심 이유 중 하나다.

5. 가능한 해결 방법들

방법 A. pdfjs 내부 API를 이용해 round-trip 직접 구현. 로드 시 /Annot 객체를 파싱해서 editor 객체로 변환하는 방식. 가능은 하지만, pdfjs 버전 올릴 때마다 깨질 위험이 크고 유지보수 비용이 라이선스비보다 비쌀 수 있다.

방법 B. 어노테이션 JSON 분리 저장. 임시저장 시 원본 PDF는 건드리지 않고, pdfjs의 AnnotationStorage를 JSON으로 직렬화해서 별도 저장. 복원 시 원본 PDF + JSON으로 에디터 객체를 재생성. 최종 제출 시에만 flatten. 이게 가장 현실적이다.

방법 C. 임시저장을 아예 없애고 제출만. 가장 단순하지만, 사용자가 오래 작업하다 브라우저가 크래시하면 모든 작업이 날아간다.

6. 현재 결론

당분간 프로덕션(/progress-homework/pdf)에서는 PSPDFKit을 유지한다. pdfjs 기반 에디터는 /labs/pdf-editor에서 계속 발전시키되, 어노테이션 영속성 문제가 해결되기 전까지는 실 서비스 교체를 보류한다.

만든 것들은 버리지 않는다. 공통 컴포넌트(common/pdf-editor/)로 잘 분리해뒀고, 썸네일 사이드바, 툴바 최소화, 보기 전용 모드 같은 UX 개선들은 나중에 PSPDFKit 위에도 적용하거나, 어노테이션 문제가 풀리면 그때 교체할 수 있다.

무료 라이브러리로 바꾸면 비용이 줄 거라 생각했는데, 유료 라이브러리가 유료인 데는 이유가 있었다. 특히 PDF처럼 30년 된 스펙 위에서 돌아가는 도메인에서는, '잘 동작하는 것'의 가치가 생각보다 크다.

7. 다음 단계 (TODO)

  • pdfjs-dist의 AnnotationStorage.serializable + deserialize() API 조사
  • 방법 B (JSON 분리 저장) 프로토타입 구현 및 테스트
  • round-trip이 되면 PSPDFKit 교체 재시도
  • 이 글을 진행 상황에 따라 업데이트

(이 글은 작업이 진행됨에 따라 계속 갱신됩니다)

  • PSPDFKit에서 pdfjs-dist로: 유료 PDF 라이브러리 교체 삽질기
  • 1. 왜 교체하려 했나
  • 2. 만들었던 것들
  • 3. 부딪힌 벽: 어노테이션 영속성
  • 4. PSPDFKit vs pdfjs-dist: 근본적인 차이
  • 5. 가능한 해결 방법들
  • 6. 현재 결론
  • 7. 다음 단계 (TODO)