목차
계약서 양식을 한 번 만들어 두고 이름과 날짜만 바꿔 쓰게 하고 싶은 적 있으시죠. 화면에서는 멀쩡해 보이던 글자가 내려받은 PDF 에서는 네모로 깨지거나, 줄이 다른 곳에서 바뀌거나, 칸 밖으로 삐져나오는 일이 생겨요.
이 글은 PDF 양식 위에 미리 칸을 잡아 두고, 사용자가 친 한글이 화면에서 본 그대로 PDF 에 들어가게 만드는 순서를 정리해요. 브라우저 안에서 pdf-lib 라는 라이브러리로 PDF 를 만드는 경우를 기준으로 해요.
혹시 이런 고민 있으신가요?
- 한글을 쳤더니 PDF 에서 글자가 네모나 물음표로 나와요.
- 화면 미리보기와 내려받은 PDF 의 줄바꿈 자리가 달라서, 열어 보기 전에는 결과를 믿을 수 없어요.
- 글이 칸보다 길 때 잘라야 할지 줄여야 할지 몰라서 아무 기준 없이 만들고 있어요.
이 글을 읽으면
- 칸의 위치와 모양은 운영자가 정하고, 사용자는 값만 넣는 구조를 설계할 수 있어요.
- 한글 글꼴을 PDF 에 싣는 방법과, 글꼴을 줄일 때 생기는 숨은 손상을 확인하는 방법을 알게 돼요.
- 넘치는 글자의 처리 순서를 정하고, 화면과 결과의 줄바꿈을 같은 자리에 맞출 수 있어요.
1단계. 칸은 운영자가 정하고 사용자는 값만 넣게 나누기
먼저 누가 무엇을 정하는지부터 나눠요. 양식 PDF 를 올리고 칸의 자리를 찍는 사람과, 그 칸에 글자를 치는 사람이 같으면 화면이 복잡해지고 실수도 늘어요.
그래서 두 역할을 나눠요. 운영자는 어느 쪽 어느 자리에 칸이 있는지, 글자 크기와 정렬은 어떤지를 정해 두고 저장해요. 사용자는 칸을 옮기거나 지우지 못하고, 칸 이름표 옆의 입력란에 값만 쳐요. 고를 것이 적을수록 채우는 속도가 빨라지기 때문이에요.
칸 하나는 이런 모양의 데이터면 충분해요. 이름은 예시예요.
{ "id": "party-a-name", "label": "갑 이름", "page": 1, "x": 120, "y": 640, "width": 180, "height": 24, "fontSize": 11, "align": "left", "kind": "text"}좌표의 단위는 PDF 의 포인트예요. 포인트는 인쇄에서 쓰는 길이 단위로 1포인트가 1/72인치예요. 화면 픽셀로 저장하면 확대 비율이 바뀔 때마다 어긋나니까, 저장은 포인트로 하고 화면에서만 비율을 곱해 그려요. 화면 좌표를 PDF 좌표로 바꾸는 방법은 쪽의 회전이나 잘라낸 영역까지 따져야 해서, 이 글에서는 다루지 않고 이미 바꿔 둔 값을 쓴다고 가정할게요.
저희는 칸마다 기본 글자 크기를 11포인트로 두고 운영자가 칸을 만들 때 정하게 했어요. 계약서 본문이 보통 10포인트에서 11포인트로 인쇄되기 때문이에요. 사용자 화면에는 글자 크기 선택지를 아예 두지 않았어요.
TIP
양식을 만들 때 "사용자가 고칠 수 있게 할까"를 칸마다 묻지 마세요. 처음에는 전부 고정하고, 고치고 싶다는 요청이 실제로 쌓인 칸만 열어 주는 편이 안전해요.
또 하나 정할 것이 있어요. 채운 값을 어디까지 보낼지예요. 양식에는 이름, 주소, 계좌번호처럼 남에게 보이기 싫은 값이 들어가기 쉬워요. 저희는 양식 파일을 서버에서 받아 오는 것까지만 서버를 쓰고, 사용자가 친 값은 브라우저를 떠나지 않게 했어요. 화면에도 "채운 내용은 보관하지 않아요"라고 적어 뒀어요. 내려받기까지 브라우저의 네트워크 기록에서 양식을 받는 요청 말고 값이 실린 요청이 없는지 직접 확인해 보세요.
운영자가 칸을 정하고, 사용자는 값만 쳐서 브라우저 안에서 PDF 를 만들어요
2단계. 한글 글꼴을 PDF 에 직접 싣고, 화면에도 같은 글꼴 쓰기
PDF 를 만드는 라이브러리는 보통 영문 기본 글꼴 몇 가지를 내장하고 있어요. pdf-lib 도 그래요. 이 기본 글꼴에는 한글 글자 모양이 들어 있지 않아서, 한글을 그리려고 하면 오류가 나거나 글자가 깨져요. 그래서 한글 글꼴 파일을 직접 가져와서 PDF 안에 넣어야 해요.
pdf-lib 에서 사용자 글꼴을 쓰려면 fontkit 이라는 보조 라이브러리를 등록해요. fontkit 은 글꼴 파일 안의 글자 모양 데이터를 읽어 주는 도구예요.
import { PDFDocument } from "pdf-lib"import fontkit from "@pdf-lib/fontkit" const pdf = await PDFDocument.load(templateBytes)pdf.registerFontkit(fontkit) const fontBytes = await fetch("/fonts/body-regular.ttf").then((r) => r.arrayBuffer())const font = await pdf.embedFont(fontBytes, { subset: false }) const page = pdf.getPage(0)page.drawText("홍길동", { x: 120, y: 640, size: 11, font })글꼴은 몇 벌 고를지부터 정해요. 저희는 한 벌만 골랐어요. 후보를 파일 크기로 비교해 봤어요. 아래 크기는 저희가 내려받아 gzip 으로 압축해 잰 값이에요. 제목용 글꼴은 더 작았지만 본문에 맞지 않아 후보에서 뺐어요.
| 후보 | 압축 크기 | 메모 |
|---|---|---|
| 계약서 본문용 정적 글꼴 | 약 1.0MB | 원본 약 2.8MB, 가장 작은 본문용 |
| 가변 글꼴 | 약 5.3MB | 굵기 축을 다루는 확인이 더 필요 |
본문용 정적 글꼴을 골랐어요. 가변 글꼴은 하나의 파일 안에서 굵기를 바꿀 수 있는 글꼴인데, 그만큼 파일이 크고 PDF 에 싣기 전에 확인할 것이 늘어요.
그리고 이 글꼴을 PDF 에만 쓰지 않고 화면 미리보기에도 똑같이 썼어요. 화면은 평소 쓰던 다른 글꼴로 그리고 PDF 만 이 글꼴로 만들면, 같은 글자라도 폭이 달라서 줄이 바뀌는 자리와 넘침 판정이 화면과 결과에서 갈려요. 화면에서는 멀쩡하던 글이 PDF 에서 넘치면 사용자는 내려받아 열어 본 뒤에야 알게 돼요. 칸 안의 미리보기만큼은 같은 파일을 @font-face 로 불러서 그리세요.
참고
한글 글꼴을 통째로 싣는 대가는 용량이에요. 저희가 글자 30개가 든 칸 10개짜리 한 쪽 PDF 를 만들어 재 보니, 원본 583바이트가 결과 953,232바이트가 됐어요. 약 930KiB 가 늘었고 합성과 내려받기는 세 번 재서 335밀리초에서 363밀리초 걸렸어요. 글자가 하나도 없는 문서에까지 글꼴을 싣지 않도록, 채울 글자가 있을 때만 글꼴을 넣는 분기를 두면 좋아요.
3단계. 글꼴을 줄이는 옵션은 손상 여부까지 확인하고 켜기
방금 코드에서 subset: false 라고 쓴 이유를 설명할게요. 서브셋은 글꼴 전체가 아니라 실제로 쓴 글자의 모양만 PDF 에 넣어서 용량을 줄이는 옵션이에요. 한글 글꼴은 글자 수가 많아서 줄어드는 폭이 크니까 켜고 싶어지죠.
문제는 pdf-lib 의 이슈 1232 가 말하듯, 큰 글꼴의 한중일 글자를 서브셋할 때 문제가 생긴다는 보고예요(https://github.com/Hopding/pdf-lib/issues/1232). 저희 글꼴에서도 재현되는지 이렇게 확인했어요.
- 빈 PDF 에 흔한 한글 단어와 드문 음절, 한글 전체 영역에 고르게 흩어진 음절 16개, 영문, 숫자를 한 줄로 그렸어요.
- 서브셋을 켜고 만든 PDF 에서 pdf.js 의 글자 추출로 문자열을 뽑았어요. 입력과 한 글자도 다르지 않았어요.
- 그런데 PDF 안에 든 글꼴 데이터를 꺼내 fontkit 으로 다시 열어 글자 모양을 처음부터 차례로 읽어 보니, 4번째 글자 모양이 비어 있었고 5번째를 읽는 순간 fontkit 이 오류로 멈췄어요.
복사해서 붙여넣을 때 읽는 글자 층은 멀쩡한데, 그 아래 글자 모양 데이터는 깨져 있었던 거예요. 글자 추출만 확인했다면 이 손상을 놓쳤을 거예요. 서브셋을 끈 결과는 PDF 에 실린 글꼴 데이터가 원본 파일과 바이트 수까지 같았어요. 사람이 손대지 않은 그대로라서 이 손상이 있을 수 없어요.
그래서 저희는 서브셋을 코드에 끄는 값으로 고정했고, 옵션으로 열어 두지 않았어요. 용량이 문제가 되는 날, 라이브러리가 고쳐졌는지 같은 시험을 다시 돌려서 확인하고 켜면 돼요.
TIP
글꼴이나 이미지를 줄이는 옵션을 켤 때는 "글자가 읽히는가"만 보지 말고, 결과 파일 안의 부품을 꺼내 다시 열어 보는 시험을 하나 만들어 두세요. 눈에 보이는 결과가 멀쩡한 손상이 가장 오래 숨어 있어요.
독자가 바꿀 점은 이거예요. 영문과 숫자만 쓰는 양식이라면 글꼴이 작으니 이 단계의 고민은 필요 없어요. 한글을 쓴다면 서브셋을 켜기 전에 위의 세 단계 시험을 먼저 돌려 보세요.
4단계. 넘치는 글자의 처리 순서를 정하기
글이 칸보다 길 때가 반드시 와요. 이름 칸에 긴 회사명을 치거나, 비고 칸에 문장을 길게 쓰는 경우예요. 이때 어떻게 할지를 코드를 쓰기 전에 순서로 정해 두세요. 저희는 이렇게 정했어요.
- 칸 너비에 맞춰 줄을 바꿔요.
- 그래도 세로로 넘치면 글자 크기를 8포인트까지 줄여요.
- 8포인트로도 넘치면 더 줄이지 않고 넘친 채로 둬요.
어느 단계에서도 글자를 자르지 않아요. 계약서에서 글자가 사라지는 건 사고인데, 자르면 사용자는 열어 본 뒤에야 알게 되기 때문이에요. 남은 선택은 칸 밖으로 넘치게 두는 것과 보이지 않게 숨기는 것인데, 숨기는 건 자르는 것과 사용자가 겪는 일이 같아요. 넘치게 두면 화면에서 바로 보여서 사용자가 칸에 맞게 글을 줄여서 고칠 수 있어요.
8포인트라는 값은 A4 인쇄물에서 읽을 수 있는 아래쪽 한계로 저희가 잡은 판단이에요. 측정하거나 어디서 인용한 값이 아니에요. 여러분은 인쇄 크기와 독자 나이에 맞춰 바꾸세요. 바꾸기 쉽게 상수 하나로 두면 돼요.
const MIN_FONT_SIZE = 8 function fitText(text: string, box: Box, measure: Measure) { for (let size = box.fontSize; size >= MIN_FONT_SIZE; size -= 0.5) { const lines = wrap(text, box.width, size, measure) if (lines.length * size * LINE_HEIGHT <= box.height) { return { size, lines, overflow: false } } } const lines = wrap(text, box.width, MIN_FONT_SIZE, measure) return { size: MIN_FONT_SIZE, lines, overflow: true }}넘친 칸은 화면에서 눈에 띄게 표시하고, 내려받기 버튼 옆에 넘친 칸이 몇 개인지 적어요. 내려받기 자체를 막지는 않아요. 넘친 글자가 다른 글씨를 가리는지는 사용자가 보고 정할 일이고, 막아 버리면 그 문서에서 다른 방법이 없는 사용자가 아무것도 못 하기 때문이에요. 막는 대신 눌러야 하는 시점에 숫자로 알려요.
TIP
"자르지 않는다"처럼 한 줄로 말할 수 있는 원칙을 먼저 정하면 나머지 판단이 쉬워져요. 줄바꿈, 축소, 표시는 그 원칙을 지키려고 쌓은 단계일 뿐이에요.
줄바꿈, 8포인트까지 축소, 넘친 채로 두고 표시하는 순서
5단계. 화면과 PDF 의 줄바꿈 자리를 실측으로 맞추기
이제 가장 까다로운 단계예요. 줄을 바꾸는 계산을 화면과 PDF 가 각자 하니까 자리가 어긋나요. 화면은 브라우저가 글자 폭을 재서 줄을 바꾸고, PDF 는 우리 코드가 글자 폭을 재서 줄을 바꾸기 때문이에요.
저희는 처음에 화면의 칸 테두리 2픽셀과 안쪽 여백 2픽셀을 더한 8픽셀을 확대 비율로 나눈 약 6포인트를 줄바꿈 너비에서 빼면 된다고 계산했어요. 결과는 틀렸어요. 모든 줄에서 화면보다 단어가 하나씩 더 들어갔어요.
그래서 이론 대신 시험으로 값을 찾았어요. 방법은 이래요.
- 짧아서 안 넘치는 글, 두 줄로 넘치는 글, 8포인트까지 줄어드는 긴 글, 세 가지를 준비해요.
- 화면에서는 글자 하나하나의 위치를 읽어 같은 줄에 속하는 글자를 모아서 줄 경계를 얻어요. 브라우저의
getClientRects()로 할 수 있어요. - 내려받은 PDF 는 pdf.js 로 읽어 줄 경계를 뽑아요.
- 줄바꿈 너비에서 빼는 값을 8, 12, 20 순서로 늘려 가며 두 경계가 같아지는지 봐요.
20포인트에서 세 경우 모두 줄이 끊기는 단어가 정확히 같아졌어요. 이론값 6포인트와 14포인트나 차이가 나는 이유는 확정하지 못했어요. 글자 폭을 재는 캔버스 측정이 실제 화면 배치와 다르거나, 한글이 공백이 아니라 음절 사이 어디서나 줄을 바꿀 수 있는 규칙과 우리의 공백 기준 줄바꿈이 다른 것이 후보예요. 확인되지 않은 가설이에요.
이유를 모르는 값을 코드에 두는 건 찜찜하지만, 저희는 근거를 계속 쫓기보다 세 경우를 실제로 통과하는 값을 쓰기로 했어요. 대신 한계를 기록해 뒀어요. 이 값은 고정된 숫자라서 화면을 많이 확대하거나 줄인 채 칸을 만들면 한두 글자 어긋날 수 있어요. 정확히 고치려면 그 시점의 확대 비율을 합성 단계까지 넘겨 값을 비율로 나눠야 해요.
위치가 맞는지도 같은 방식으로 쟀어요. 칸을 옮긴 뒤 내려받은 PDF 에서 pdf.js 로 글자 위치를 뽑았더니 가로 오차가 0.04포인트, 세로 오차가 약 0.1포인트였어요. 1포인트 안이라는 기준을 넉넉히 통과했어요.
마지막으로 내려받은 PDF 에서 글자를 복사해 붙여넣었을 때 입력한 문자열과 같은지도 확인하세요. 글자를 이미지로 그리지 않고 글자로 그렸다는 증거예요.
참고
암호가 걸린 PDF 를 걸러낼 때, pdf-lib 1.17.1 에서는 오류 종류를 instanceof 로 비교하면 같은 모듈 안에서도 거짓이 나오는 일이 있었어요. 오류 메시지에 "is encrypted"가 들어 있는지 보는 방법을 함께 두어 해결했어요.
| 시험 | 맞춘 방법 | 통과 기준 |
|---|---|---|
| 줄바꿈 자리 | 화면 줄 경계와 PDF 줄 경계 대조 | 세 길이에서 끊기는 단어가 같음 |
| 글자 위치 | pdf.js 로 위치를 읽어 대조 | 오차 1포인트 이내 |
| 글꼴 손상 | 글꼴 데이터를 꺼내 차례로 읽기 | 오류 없이 끝까지 읽힘 |
| 복사 | 내려받은 PDF 에서 글자 복사 | 입력과 같은 문자열 |
자주 묻는 질문
Q1. 글자를 이미지로 그리면 글꼴 걱정이 없지 않나요?
글꼴 걱정은 사라지지만 그 대가가 커요. 이미지는 PDF 에서 복사하거나 검색할 수 없고, 확대하면 흐려지거나 용량이 커져요. 계약서처럼 다시 읽을 문서는 글자로 남기는 편이 좋아요.
Q2. 영문 양식에도 이 과정이 다 필요한가요?
대부분 필요 없어요. pdf-lib 의 기본 영문 글꼴로 충분하고 글꼴을 싣지 않으니 용량도 늘지 않아요. 다만 줄바꿈과 넘침 처리의 원칙은 언어와 관계없이 그대로 쓸 수 있어요.
Q3. 줄바꿈 값을 이론으로 정확히 구할 수는 없나요?
저희는 원인을 확정하지 못했어요. 글자 폭을 재는 방식과 줄을 바꿀 수 있는 위치의 규칙이 화면과 코드에서 서로 다르다는 의심만 있어요. 화면과 결과를 같은 엔진으로 그리는 방법도 있지만, 그만큼 구조가 무거워져서 저희는 실측으로 맞췄어요.
정리
- 칸의 위치와 모양은 운영자가 포인트 단위로 정하고, 사용자는 값만 넣게 해요.
- 한글은 글꼴을 직접 싣고, 화면 미리보기도 같은 글꼴로 그려서 폭을 맞춰요.
- 서브셋 같은 줄이기 옵션은 결과 파일 안의 부품을 다시 열어 보는 시험을 통과한 뒤에 켜요.
- 넘치는 글은 줄바꿈, 8포인트까지 축소, 넘친 채 표시의 순서로 다루고 절대 자르지 않아요.
- 화면과 PDF 의 줄바꿈은 이론값보다 실측으로 맞추고, 아는 한계를 기록해 둬요.
PDF 를 열어 칸을 찍고 글자를 채워 내려받는 일은 로그인 없이 해 볼 수 있어요.