
TOPIA Live 서비스는 수업 전후 학생의 학습 효과를 높이기 위해 학생들에게 과제를 배정합니다. 과제는 여러 형태로 제공되고 있으며, 그중에는 교재 일부를 풀어 제출하는 유형이 존재합니다. 해당 유형은 교재에 직접 풀이한 뒤 사진을 찍어 제출하거나, 온라인에서 PDF 위에 바로 풀이해서 제출할 수도 있습니다.
초기에는 학생이 과제를 직접 풀고 사진을 찍어 업로드하는 방식만 제공했습니다. 하지만 과제를 완료한 뒤 모바일 기기로 촬영하고, 다시 서비스에 업로드해야 하는 과정은 학생과 학부모 모두에게 번거로운 문제가 있었습니다. 이를 개선하기 위해 교재 PDF를 편집기에서 바로 열고, 그 위에 풀이를 작성해 제출할 수 있도록 PSPDFKit이 도입되어 있는 상태였습니다.
별도의 장애나 문의가 없어 잘 동작하고 있다고 믿고 신경을 크게 쓰지 않고 있었으나, 한 학부모로부터 아이가 PDF 과제를 하는 도중 이상한 글자가 보인다는 이슈를 제보받았습니다. 확인해보니 PSPDFKit 워터마크가 PDF 편집 화면에 노출되고 있었고, 일부 제출물에는 해당 워터마크가 PDF에 포함된 채 저장되는 문제까지 발생하고 있었습니다.
더 확인해보니 PSPDFKit의 정식 라이선스가 아니라 Trial Key가 사용되고 있었습니다. 이 키는 일정 주기마다 갱신이 필요했지만, 담당자가 바뀌는 과정에서 갱신 작업이 명확히 인수인계되지 않았습니다. 그 결과 키가 만료되었고, 워터마크가 사용자 화면과 제출물에 노출되고 있었습니다.
학생이 제출한 과제 결과물에 외부 라이브러리의 워터마크가 남는 상황은 서비스 신뢰도에 직접적인 영향을 줄 수 있는 심각한 문제라고 판단하였습니다. 빠르게 문제를 수습하면서도, 같은 문제가 반복되지 않도록 비용과 운영 부담까지 함께 줄일 수 있는 대안을 찾아야 했습니다.
문제는 이 작업에만 전념할 수 있는 상황이 아니었다는 점입니다. 메인으로 맡고 있던 프로젝트가 따로 있었기 때문에, PDF 과제 기능 개선은 기존 업무와 병행해서 진행해야 했고, 일정도 넉넉하지 않았습니다.
기존 PSPDFKit에서 학생들이 실제로 사용하던 기능과 제출 플로우를 빠르게 파악하고, 대체 구현부터 테스트까지 마무리해야 했습니다. 단순한 라이브러리 교체가 아니라, 제한된 시간 안에서 기존 사용성을 최대한 유지해야 하는 챌린지 상황이었습니다.
PSPDFKit 정식 라이선스 구매이미 서비스에 연동되어 있던 라이브러리였기 때문에, 가장 빠르고 간단하게 문제를 해결할 수 있는 선택지였습니다. 기존 구현을 크게 건드리지 않아도 되고, 워터마크 문제도 라이선스만 정상화하면 바로 해결할 수 있었습니다.
하지만, AWS 인프라 비용을 포함해 여러 구독형 서비스 비용을 지출하고 있는 TOPIA Live 특성 상, PDF 편집 라이브러리 비용까지 장기적으로 더해지는 구조는 부담스러웠습니다. 따라서, 지속 가능한 해결책이라고 보기는 어려웠습니다.
pdfjs-dist 라이브러리 뷰어 + 커스텀 에디터 개발기존에 사용하고 있던 pdfjs-dist로 PDF 뷰어를 구성하고, 그 위에 필요한 편집 기능을 직접 구현하는 방법이었습니다. pdfjs-dist로 PDF를 렌더링한 뒤, 필기, 텍스트 입력, 지우개 같은 기능을 커스텀 에디터로 얹는 방식입니다.
필요한 기능이 전문 PDF 편집기 수준이 아니라, 학생이 교재 위에 필기하고 수정할 수 있는 정도면 충분하다고 생각했기 때문에 필요한 기능만 직접 통제하여 커스텀 에디터를 구현하기로 채택했습니다.
처음에는 pdfjs-dist가 PDF를 안정적으로 렌더링해주고 있으니, 그 위에 펜, 하이라이트, 지우개 같은 도구만 얹으면 될 거라고 안일하게 생각했습니다. 학생이 사용하는 기능도 복잡하지 않았고, AI 에이전트와 함께 구현하면 제한된 일정 안에서도 충분히 해볼 만하다고 판단했습니다.
그런데 막상 구현해보니 가장 기본적인 지우개 동작부터 생각보다 까다로웠습니다. PDF 위에 마우스를 이용해 펜을 그리고 지우면 이 선은 Cubic Bezier라는 곡선 데이터로 저장됩니다. 쉽게 말해 선 전체를 수많은 픽셀로 저장하는 것이 아니라, 시작점과 끝점, 그리고 곡선의 휘어짐을 결정하는 제어점들을 저장해두고 그 정보로 부드러운 선을 다시 그리는 방식입니다.
문제는 지우개가 지나간 부분만 지울 때 이 곡선을 중간에서 자르는 부분에서 발생하였습니다. 곡선의 일부를 잘라내고 남은 구간만 다시 이어붙이는 과정에서, 원래 선이 가지고 있던 “얼마나 자연스럽게 휘어졌는지”에 대한 정보가 그대로 유지되지 않았습니다. 남은 점들을 단순히 다시 연결하다 보니, 처음에는 부드럽게 그려진 선이 지우개를 몇 번 거칠수록 점점 짧은 직선 조각들이 이어진 형태로 바뀌었습니다.
또한, 저장 후 다시 불러온 PDF에서는 좌표계 문제가 발생하기도 했습니다. 사용자가 화면에서 문지르는 위치는 브라우저 화면 기준 좌표이고, 펜 경로는 PDF 내부 좌표로 저장되어 있어 둘 사이를 변환해야 합니다. 처음 편집할 때는 pdf.js 에디터 객체가 들고 있는 scaleFactor, translationX/Y 같은 런타임 값을 사용해 좌표를 맞출 수 있었지만, 이 값들은 PDF 파일에 저장되는 정보가 아니었습니다. 그래서 S3에서 PDF를 다시 불러오면 이전과 같은 변환 값이 복원되지 않았고, 화면상으로는 선 위를 지우고 있는데 내부적으로는 다른 위치를 지우는 것으로 계산되어 아무것도 지워지지 않는 문제가 생겼습니다.
이 방식으로 계속 밀어붙이기에는 한계가 분명했습니다. 시행착오를 반복하면 눈앞의 문제는 하나씩 막을 수 있었지만, 저장과 복원, 재편집까지 안정적으로 보장해야 하는 기능을 이런 방식으로 계속 패치해나가기는 어려웠습니다. 게다가 비슷한 시기에 PDF뿐만 아니라 이미지 파일도 편집할 수 있게 해달라는 요청이 들어왔고, 이미지 업로드 방향에 따른 회전, 반전 기능도 필요해졌습니다. 여기에 기존에는 사용하지 않던 도형 그리기와 스티커 붙이기 기능까지 요구사항에 포함되면서, 단순히 PDF 위에 필요한 도구를 하나씩 얹는 방식으로는 감당하기 어렵다고 판단했습니다. 결국 PDF와 이미지를 함께 다루면서도 편집 기능을 안정적으로 제공할 수 있는 다른 방법을 찾아야 했습니다.
pdf.js 오픈소스 포크대안을 찾기 위해 GitHub를 살펴보던 중, Mozilla가 만들고 관리하는 pdf.js 라는 라이브러리 발견하게 됐습니다. 처음에는 PDF를 화면에 보여주는 뷰어 라이브러리 정도로 생각했지만, 실제로는 렌더링뿐 아니라 에디터 기능까지 포함하고 있었습니다. 특히 펜, 텍스트, 하이라이트, 지우개처럼 TOPIA Live에서 필요로 하던 기능이 상당수 들어 있다는 점이 눈에 띄었습니다.
코드를 살펴보니 pdf.js는 크게 PDF를 해석하는 core layer, 화면에 렌더링하는 viewer layer, 사용자의 입력을 관리하는 annotation editor layer로 나뉘어 있었습니다. core layer가 PDF 파일을 읽고 페이지 정보를 해석하면, viewer layer가 이를 화면에 그립니다. 그 위에서 annotation editor layer가 펜, 텍스트, 하이라이트 같은 편집 요소를 생성하고 관리하는 구조였습니다. PDF 원본을 렌더링하는 영역과 사용자가 추가한 편집 데이터를 다루는 영역이 분리되어 있다는 점이 마음에 들었습니다.
저장 흐름도 이 구조 안에서 이어졌습니다. annotation editor layer에서 생성된 입력은 화면 위에만 그려지는 임시 요소가 아니라 editor type으로 관리되고, annotation storage에 저장됩니다. 사용자가 저장을 요청하면 pdf.js는 이 데이터를 PDF에 반영해 새로운 PDF 바이트를 생성합니다. 서비스에서는 이 결과를 exportPDF()로 받아 S3에 업로드하면 됐고, 다시 열었을 때도 기존 입력이 PDF annotation으로 복원되어 이어서 확인하거나 수정할 수 있었습니다.
이 정도 구조라면 별도의 커스텀 에디터를 계속 덧붙이는 것보다, pdf.js를 포크해 TOPIA Live의 과제 제출 흐름에 맞게 다듬는 편이 낫겠다고 판단했습니다. 이후 UI/UX를 서비스에 맞게 정리하고, 필요한 기능을 고도화한 뒤 내부 패키지로 배포하는 방향으로 진행했습니다.
기본으로 제공되는 펜, 텍스트, 하이라이트, 지우개는 학생 과제 제출 화면에서 바로 사용할 수 있도록 붙이고, 새로운 요구사항이었던 강사 첨삭에 필요한 도형 그리기와 스티커 붙이기 기능을 구현했습니다. 도형은 Ink annotation(펜) 구조를 재사용해 사각형, 삼각형, 타원을 포함한 도형을 그릴 수 있도록 유도했고, 스티커는 Stamp annotation 기반으로 자체 제작한 SVG 스티커 이미지를 PDF 위에 배치할 수 있도록 구현했습니다.
이미지 제출물도 같은 에디터에서 다룰 수 있도록 처리했습니다. 학생이 사진으로 제출한 과제는 PDF 뷰어에서 바로 열 수 없기 때문에, 진입 시 이미지 Blob을 한 페이지짜리 PDF로 변환해 에디터에 전달했습니다. 덕분에 PDF 과제와 이미지 과제를 별도 화면으로 나누지 않고, 같은 편집 도구와 저장 흐름 안에서 다룰 수 있었습니다.
이렇게 고도화한 에디터는 Next.js 프로젝트에서 선언적으로 사용할 수 있도록 React Wrapper로 감쌌습니다. 내부에서는 HTML 기반으로 동작하는 pdf.js 뷰어를 iframe에 로드하고, 서비스 코드에서는 문서 경로와 사용 모드만 props로 넘기면 에디터가 열리도록 구성했습니다. 저장이나 undo/redo처럼 명령형으로 호출해야 하는 동작은 ref API로 노출했습니다. 사용하는 쪽에서는 PDF.js 내부 구조를 직접 다루지 않고도 과제 제출, 강사 첨삭, 뷰어 모드를 같은 컴포넌트로 사용할 수 있게 되었습니다.
다만 오픈소스 라이브러리 위에서 고도화한다고 해서 모든 문제가 사라진 것은 아니었습니다. 지우개만 해도 도구 하나처럼 보이지만, 실제로는 펜, 하이라이트, 도형마다 지워야 하는 데이터 구조가 달랐습니다. 하이라이트를 지울 때 mouse-up 시점에 의도하지 않은 구간까지 사라지거나, 연속으로 드래그하면 일부 구간이 누락되는 문제가 있었습니다. 도형과 펜 stroke도 저장 후 다시 불러왔을 때 hit detection을 다시 맞춰야 했습니다. 화면에서는 비슷한 선처럼 보여도 내부적으로는 각 도구가 좌표와 경로를 다루는 방식이 달랐기 때문에, 지우개 하나를 안정화하는 데도 여러 차례 수정이 필요했습니다.
저장과 복원 흐름에서도 시행착오가 이어졌습니다. FreeText는 저장 후 다시 불러오면 위치가 흔들리거나 보기 모드에서 잔상이 남는 문제가 있었고, 한글과 이모지를 PDF에 저장할 때는 폰트 임베딩 문제도 따로 처리해야 했습니다. 여기에 강사 모드에서 페이지 회전과 좌우 반전 기능까지 추가되면서, 펜, 하이라이트, 도형, 스티커의 좌표를 모두 다시 맞춰야 했습니다. 결국 저장 후 다시 불러오는 과정을 빠르게 재현할 수 있는 개발 도구까지 만들고, 실제 사용 흐름에 가깝게 반복 검증하면서 하나씩 안정화해나갔습니다.
결과적으로 PSPDFKit을 대체하면서도 TOPIA Live에 필요한 PDF 편집 기능을 내부 패키지로 운영할 수 있게 되었습니다. 공개 가격 자료 기준으로 보면 PSPDFKit은 사용자 약 700명 기준 공개 견적 사례를 참고하면 연 3천만 원 안팎 이어질 수 있는 구독형 SDK였습니다. 이를 pdf.js 기반 내부 라이브러리로 전환하면서, 단순히 워터마크 문제를 해결한 것을 넘어 매년 반복될 수 있는 라이선스 비용과 갱신 리스크를 함께 줄일 수 있었다는 점에서 굉장히 뜻깊은 기여였다고 생각합니다.
기존 pdf.js 뷰어의 개발자 도구 같은 딱딱한 UI를 갈아엎어 디자인에도 직접 기여했습니다. 학생 과제 제출과 강사 첨삭이라는 사용 흐름에 맞게 툴바, 도구 배치, 저장 흐름을 다시 정리했고, 그 과정에서 프로젝트 안에 흩어져 있던 PDF 뷰어 사용 방식도 함께 정리했습니다. 기존에는 화면마다 다른 라이브러리와 구현 방식으로 PDF를 열고 있었는데, 이번에 만든 내부 패키지로 교체하면서 PDF를 다루는 진입점을 하나로 모았습니다. 덕분에 기능 개선이나 오류 대응도 한 곳에서 할 수 있는 구조가 되었습니다.
처음에는 사용량이 크지 않았던 기능이었지만, 새 학기가 시작되면서 상황이 달라졌습니다. 현재는 전체 과제의 50% 이상이 이 PDF 에디터 흐름으로 진행되고 있고, 운영 중 발생한 크리티컬 이슈는 0건이었습니다. 자잘한 이슈 2건을 제외하면 대부분은 버그 리포트가 아니라 추가 요구사항이었고, 강사들의 만족도도 매우 높았습니다. 직접 포크한 오픈소스를 서비스에 맞게 고도화하고, 비용 절감과 사용자 경험 개선까지 함께 만들어낸 작업이라 특히 기억에 남습니다.