
PSPDFKit 라이브러리를 도입했습니다.PSPDFKit은 유료 구독형 PDF 뷰어/편집 라이브러리로, TOPIA Live 규모의 서비스에서 감당하기에는 비용 부담이 컸습니다. 그렇다고 동일한 기능을 직접 개발하기에는 공수가 너무 많이 들어 현실적으로 어려운 상황이었습니다.PSPDFKit의 라이선스 만료로 인해 워터마크가 노출되고 있었으며, 워터마크가 PDF에 포함되어 저장되는 문제까지 발생하였습니다.원인을 파악해보니, PSPDFKit의 정식 라이선스가 아닌 Trial Key를 사용하고 있었으며, 해당 키는 일정 주기마다 갱신이 필요했습니다. 그러나 담당자 변경 과정에서 이 갱신 작업이 인수인계되지 않았고, 결과적으로 키가 만료되면서 워터마크가 노출된 것이었습니다.
PSPDFKit 정식 라이선스 구매 : 가장 간단한 해결책이지만, aws 인프라 비용을 포함한 다른 구독 서비스에 대한 지출이 많은 터라 지속하기는 쉽지 않았습니다.pdfjs-dist 라이브러리 뷰어 + 커스텀 에디터 직접 구현 : pdfjs-dist로 PDF를 렌더링하고, 그 위에 필기·텍스트·지우개 등 편집 기능을 직접 구현하는 방식입니다. 라이선스 비용 없이 필요한 기능을 자유롭게 만들 수 있다는 장점이 있습니다.위 선택지 중에서, 기존에 PDF 뷰어로 사용하고 있던 pdfjs-dist 라이브러리를 재활용하여, 그 위에 커스텀 에디터를 만드는 방식으로 구현하기로 결정했습니다. pdfjs-dist는 pdf.js의 npm 배포판으로, PDF 렌더링뿐 아니라 펜, 텍스트 입력 등 기본적인 어노테이션 편집 기능도 제공하는 오픈소스 라이브러리입니다. 당시 프로젝트에 설치되어 있던 버전은 v3.4.120이었으며, 서비스에 필요한 지우개와 하이라이트 기능이 포함되어 있지 않았습니다. 중간 단계의 기능을 제공받아 덧붙이기보다는, 필요한 기능(펜, 하이라이트, 지우개, 텍스트)을 처음부터 직접 구현하는 편이 낫겠다고 판단했습니다.
PDF는 pdfjs-dist의 PDFViewer 클래스를 통해 캔버스에 렌더링하고, 그 위에 편집 레이어를 올리는 구조로 구현했습니다. 사용자가 펜으로 선을 그으면, 그 궤적은 Bezier 곡선이라는 수학적 곡선 형태로 저장됩니다. 실제 손글씨처럼 부드러운 선을 표현하기 위해 직선이 아닌 곡선으로 저장하였습니다.
지우개는 이 Bezier 곡선 데이터를 직접 조작하는 방식으로 구현했습니다. 지우개가 지나간 영역의 곡선 구간을 찾아내고, 해당 부분을 제거한 뒤, 남은 부분을 다시 새로운 곡선으로 이어붙이는 방식입니다. 곡선은 연속된 데이터이기 때문에, 중간을 잘라내면 나머지를 다시 계산해서 연결해야 합니다.
펜으로 그린 부드러운 곡선을 지우개를 사용하여 지웠더니 점점 직선에 가까운 조각들로 변환되는 문제가 발생했고, 반복 사용할수록 펜으로 그린 경로는 점점 왜곡되었습니다.
pdf.js의 잉크 에디터는 펜 경로를 Cubic Bezier 곡선(4점: 시작점, 제어점 2개, 끝점)으로 저장합니다. 지우개로 경로 일부를 지울 때, 이 Bezier 곡선을 일정 간격(6단계)으로 샘플링하여 지우개 영역과 겹치는 구간을 잘라내고, 남은 점들을 다시 이어붙여 새로운 경로로 재구성합니다.
문제는 이 재구성 과정에서 원래의 부드러운 Bezier 곡선이 직선 보간(linear interpolation)으로 대체된다는 점입니다. 원본은 곡률 정보를 가진 Bezier였지만, 지우개가 남긴 잔여 점들을 잇는 새 경로는 [P0, P0, P1, P1] 형태의 퇴화된 Bezier, 사실상 직선으로 연결됩니다(toLineBezierPath). 샘플링 해상도(6단계)도 원본 곡선을 충분히 복원하기에는 부족하여, 지우개를 반복 사용할수록 곡선이 각지고 경로가 점점 원본에서 벗어나는 현상이 누적되었습니다.
또한, PDF를 S3에 저장하고 다시 불러오면, 기존에 그린 펜이나 하이라이트를 지우개로 지울 수 없는 이슈가 발생하기도 했습니다.
원인은 좌표계의 불일치였습니다. 커스텀 지우개는 화면상의 터치 좌표를 펜 경로의 로컬 좌표로 변환하기 위해 캔버스의 scaleFactor, translationX/Y 등의 변환 값을 사용합니다. 그런데 이 값들은 pdf.js 에디터 객체의 런타임 내부 속성으로, PDF 파일 자체에 저장되지 않습니다. 저장 전에는 편집 과정에서 생성된 에디터 객체가 이 값들을 그대로 들고 있지만, S3에서 다시 불러온 PDF는 pdf.js가 Annotation을 새로 렌더링하면서 에디터 객체를 재구성하기 때문에, 이전과 동일한 변환 값이 복원된다는 보장이 없습니다. 결과적으로 지우개가 계산하는 좌표와 실제 펜 경로의 좌표가 어긋나면서, 분명히 경로 위를 지우고 있는데도 지워지지 않는 현상이 발생한 것입니다.
이 문제는 PSPDFKit에서는 발생하지 않는 문제입니다. PSPDFKit은 지우개를 포함한 모든 편집 도구가 SDK 내장이기 때문에, 주석의 생성·저장·복원·재편집에 이르는 전체 라이프사이클을 하나의 시스템이 일관되게 관리합니다. 반면 직접 구현한 방식은 pdfjs-dist가 만든 에디터 객체의 내부 속성을 외부 커스텀 코드가 직접 읽어와 좌표 변환에 사용하는 구조이기 때문에, 저장·복원 과정에서 이 내부 상태가 달라지면 바로 깨질 수밖에 없는 한계가 있었습니다.
두 가지 트러블슈팅을 거치며, 외부에서 기능을 덧붙이는 방식에는 구조적인 한계가 있다는 것이 분명해졌습니다. pdfjs-dist가 공개하지 않는 내부 속성에 의존하는 이상, 버전이 바뀌거나 저장·복원 경로가 달라질 때마다 같은 종류의 문제가 반복될 수밖에 없었습니다.
대안을 찾기 위해 pdf.js의 GitHub를 살펴보던 중, 최신 개발 버전(v5.7.0)에는 지우개, 하이라이트 등 이전 버전에 없던 편집 도구가 이미 내장되어 있다는 것을 확인했습니다. 커스텀으로 만들어 붙였던 기능들이 라이브러리 내부에서 좌표계까지 일관되게 처리되고 있었고, 이 위에서 작업하면 앞서 겪은 문제들을 근본적으로 해소할 수 있겠다는 판단이 들었습니다.
이에 해당 버전을 직접 포크하여 TOPIA에 맞게 고도화한 뒤, 내부 라이브러리로 배포하여 사용하는 방향을 선택했습니다.
포크 후 가장 먼저 해결한 것은 여전히 남아 있던 저장·복원 문제였습니다. pdf.js v5.7.0의 내장 지우개로 전환했음에도, 저장된 PDF를 다시 불러오면 잉크 주석이 정상적으로 인식되지 않는 경우가 있었습니다.
원인은 잉크 경로의 바운딩 박스(bbox) 계산에 있었습니다. PDF를 저장하고 다시 역직렬화할 때, 경로의 마진 클램핑 과정에서 bbox의 width나 height가 0 이하 또는 비정상 값이 되는 경우가 발생했고, 이로 인해 SVG viewBox가 유효하지 않게 되면서 잉크 에디터 전체가 렌더링되지 않거나, 지우개가 해당 경로를 인식하지 못하는 문제로 이어졌습니다. 원본 pdf.js에는 이에 대한 방어 로직이 없었기 때문에, bbox 계산 직후 width/height가 0 이하이거나 유한하지 않은 값일 경우 최소 크기(1e-4)로 보정하는 가드를 추가하여 해결했습니다.
이어서 다음 작업을 거쳐 내부 라이브러리로 배포에 성공하였습니다.
결과적으로, 구독료가 발생하던 PSPDFKit을 완전히 대체하면서도 동일한 수준의 PDF 편집 기능들(펜, 하이라이트, 지우개, 텍스트, 이미지 삽입, Undo/Redo, 저장 및 복원)을 구독료 없이 제공할 수 있게 되었습니다.
이번 작업에서 가장 인상 깊었던 것은, 오픈소스를 단순히 가져다 쓰는 것에서 한 발 더 나아가 직접 포크하여 내부 라이브러리로 운영하는 경험이었습니다. 처음에는 pdfjs-dist 라이브러리 위에 기능을 얹는 방식으로 접근했지만, 라이브러리의 바깥에서 내부 동작을 제어하려 할수록 구조적인 한계에 부딪혔습니다. 포크로 전환한 뒤에는 문제의 원인을 소스 레벨에서 직접 찾아 수정할 수 있었고, UI와 동작을 서비스에 맞게 자유롭게 고도화할 수 있었습니다. 외부 라이브러리에 종속되어 우회책을 쌓아가는 것과, 코드의 소유권을 가지고 근본적으로 해결하는 것 사이의 차이를 체감한 경험이었습니다.