
PSPDFKit 라이브러리를 도입했습니다.PSPDFKit은 유료 구독형 PDF 뷰어/편집 라이브러리로, TOPIA Live 규모의 서비스에서 감당하기에는 비용 부담이 컸습니다. 그렇다고 동일한 기능을 직접 개발하기에는 공수가 너무 많이 들어 현실적으로 어려운 상황이었습니다.PSPDFKit의 라이선스 만료로 인해 워터마크가 노출되고 있었으며, 워터마크가 PDF에 포함되어 저장되는 문제까지 발생하였습니다.원인을 파악해보니, PSPDFKit의 정식 라이선스가 아닌 Trial Key를 사용하고 있었으며, 해당 키는 일정 주기마다 갱신이 필요했습니다. 그러나 담당자 변경 과정에서 이 갱신 작업이 인수인계되지 않았고, 결과적으로 키가 만료되면서 워터마크가 노출된 것이었습니다.
PSPDFKit 정식 라이선스 구매 : 가장 간단한 해결책이지만, 다른 구독 서비스에 대한 지출도 많은 터라 지속하기는 쉽지 않았습니다.pdfjs-dist 라이브러리 뷰어 + 커스텀 에디터 직접 구현 : pdfjs-dist로 PDF를 렌더링하고, 그 위에 필기·텍스트·지우개 등 편집 기능을 직접 구현하는 방식입니다. 라이선스 비용 없이 필요한 기능을 자유롭게 만들 수 있다는 장점이 있습니다.위 선택지 중에서, 기존에 PDF 뷰어로 사용하고 있던 pdfjs-dist 라이브러리를 그대로 활용하고, 그 위에 커스텀 에디터를 만드는 방식으로 구현하기로 결정했습니다. pdfjs-dist는 pdf.js의 npm 배포판으로, PDF 렌더링뿐 아니라 펜, 텍스트 입력 등 기본적인 어노테이션 편집 기능도 제공하는 오픈소스 라이브러리입니다. 당시 프로젝트에 설치되어 있던 버전은 v3.4.120이었으며, 서비스에 필요한 지우개와 하이라이트 기능이 포함되어 있지 않았습니다. 중간 단계의 기능을 제공받아 덧붙이기보다는, 필요한 기능(펜, 하이라이트, 지우개, 텍스트)을 처음부터 직접 구현하는 편이 낫겠다고 판단했습니다.
PDF는 pdfjs-dist의 PDFViewer 클래스를 통해 캔버스에 렌더링하고, 그 위에 편집 레이어를 올리는 구조로 구현했습니다. 사용자가 펜으로 선을 그으면, 그 궤적은 Bezier 곡선이라는 수학적 곡선 형태로 저장됩니다. 직선이 아닌 곡선으로 저장하는 이유는, 실제 손글씨처럼 부드러운 선을 표현하기 위해서입니다.
지우개는 이 Bezier 곡선 데이터를 직접 조작하는 방식으로 구현했습니다. 지우개가 지나간 영역의 곡선 구간을 찾아내고, 해당 부분을 제거한 뒤, 남은 부분을 다시 새로운 곡선으로 이어붙이는 방식입니다. 곡선은 연속된 데이터이기 때문에, 중간을 잘라내면 나머지를 다시 계산해서 연결해야 합니다.
하지만, 펜으로 그린 부드러운 곡선을 지우개를 사용하여 지웠더니 점점 직선에 가까운 조각들로 변환되는 문제가 발생했고, 반복 사용할수록 경로는 점점 왜곡되었습니다.
pdfjs의 잉크 에디터는 펜 경로를 Cubic Bezier 곡선(4점: 시작점, 제어점 2개, 끝점)으로 저장합니다. 지우개로 경로 일부를 지울 때, 이 Bezier 곡선을 일정 간격(6단계)으로 샘플링하여 지우개 영역과 겹치는 구간을 잘라내고, 남은 점들을 다시 이어붙여 새로운 경로로 재구성합니다.
문제는 이 재구성 과정에서 원래의 부드러운 Bezier 곡선이 직선 보간(linear interpolation)으로 대체된다는 점입니다. 원본은 곡률 정보를 가진 Bezier였지만, 지우개가 남긴 잔여 점들을 잇는 새 경로는 [P0, P0, P1, P1] 형태의 퇴화된 Bezier, 사실상 직선으로 연결됩니다(toLineBezierPath). 샘플링 해상도(6단계)도 원본 곡선을 충분히 복원하기에는 부족하여, 지우개를 반복 사용할수록 곡선이 각지고 경로가 점점 원본에서 벗어나는 현상이 누적되었습니다.
지우개는 베지어 곡선을 일정 간격으로 샘플링하여 지울 영역을 판별하고, 남은 점들로 곡선을 재구성하는 방식으로 동작합니다. 문제는 이 재구성 과정에 있었습니다. 원래의 부드러운 곡선이 직선에 가까운 조각들로 변환되면서, 지우개를 반복 사용할수록 경로가 점점 왜곡되었습니다.
▎ ▎ --- ▎ Trouble Shooting ▎ ▎ 커스텀 에디터를 구현한 뒤, 두 가지 심각한 문제가 발생했습니다. ▎ ▎ 1. 지우개 사용 시 펜 경로가 깨지는 현상 ▎ ▎ 지우개는 베지어 곡선을 일정 간격으로 샘플링하여 지울 영역을 판별하고, 남은 점들로 곡선을 재구성하는 방식으로 동작합니다. 문제는 이 재구성 과정에 있었습니다. 원래의 부드러운 곡선이 직선에 가까운 조각들로 변환되면서, 지우개를 반복 사용할수록 경로가 점점 왜곡되었습니다. ▎ ▎ 2. S3 저장 후 펜·하이라이트가 지워지지 않는 현상 ▎ ▎ PDF를 S3에 저장하고 다시 불러오면, 기존에 그린 펜이나 하이라이트를 지우개로 지울 수 없었습니다. 원인은 좌표계의 불일치였습니다. 커스텀 지우개는 캔버스의 scaleFactor, translationX/Y 등 변환 값을 기반으로 펜 경로의 위치를 계산하는데, 이 값들이 PDF 저장·불러오기 과정에서 보존되지 않았습니다. 저장 전의 좌표계와 불러온 뒤의 좌표계가 어긋나면서, 지우개가 펜 경로의 위치를 찾지 못하게 된 것입니다. ▎ ▎ 두 문제의 근본 원인은 동일했습니다. pdfjs-dist가 관리하는 데이터를 라이브러리 외부에서 조작하다 보니, 내부의 데이터 구조 및 좌표계와 커스텀 로직 간의 정합성을 보장할 수 없었습니다. ▎ ▎ --- ▎ pdf.js 오픈소스 포크를 통한 해결 ▎ ▎ 외부에서 기능을 덧붙이는 방식의 한계가 명확했기 때문에, 접근 방식 자체를 전환하기로 했습니다. pdf.js의 최신 개발 버전(v5.7.0)에는 지우개, 하이라이트 등 이전 버전에 없던 편집 도구가 이미 내장되어 있었습니다. 이를 직접 포크하여 TOPIA에 맞게 고도화한 뒤, @topia-live/pdfjs-dist라는 내부 라이브러리로 배포하여 사용하는 방향을 선택했습니다. ▎ ▎ 기존 방식과 포크 방식의 차이 ▎ ▎ | | pdfjs-dist + 커스텀 에디터 | pdf.js 포크 | ▎ |---------------|-----------------------------------------------|-------------------------------------------| ▎ | 버전 | v3.4.120 (npm 배포판) | v5.7.0 소스를 포크하여 빌드 | ▎ | 지우개 | 라이브러리 외부에서 베지어 곡선을 직접 조작 | 라이브러리 내장 지우개 (폴리라인 클리핑) | ▎ | 좌표계 | 캔버스 픽셀 좌표 → 로컬 좌표 변환 필요 | 정규화 좌표(0~1) 기반, 통일된 좌표계 | ▎ | 저장/불러오기 | 변환 메타데이터가 소실되어 좌표 불일치 발생 | 동일한 좌표계가 유지되어 불일치 없음 | ▎ | 경로 품질 | 지우개 사용 시 곡선 재구성 과정에서 품질 열화 | 원본 경로를 정밀하게 클리핑하여 품질 유지 | ▎ ▎ 핵심 차이는 지우개가 어디서 동작하느냐에 있습니다. 이전 방식은 라이브러리가 관리하는 데이터를 외부에서 조작했기 때문에, 라이브러리의 내부 좌표계·직렬화 방식과 충돌이 발생했습니다. 포크 방식은 라이브러리 내부에서 모든 편집이 이루어지기 때문에, 좌표계가 처음부터 끝까지 일관되고, 저장 후 다시 불러와도 동일하게 동작합니다. ▎ ▎ 포크 후 TOPIA에 맞게 추가한 작업은 다음과 같습니다. ▎ ▎ - 뷰어 UI 전면 개편 ▎ - 한국어 로케일 지원 ▎ - 읽기 전용 모드 ▎ - React 래퍼 컴포넌트 개발 및 @topia-live/pdfjs-dist로 GitHub Packages 배포