2026.07 – 현재 · DigitalPresso
HWPX 문서 렌더러
한컴오피스 없이 브라우저에서 HWPX 원본 레이아웃을 재현하는 렌더러
FrontendBackendPythonlxmlTypeScriptNext.js 15React 19PlaywrightvitestpytestaiohttpMCPturborepo
- 레이아웃 정확도
- 미측정→한컴 캐시 대비 0.5mm 이내
- 최악 페이지 넘침
- +1,969mm→+10.6mm
- 그림 옆 본문 폭
- 40mm→117.7mm
- 쪽번호 렌더
- 0개→26/26
- 웹폰트 전송량
- 7.6MB→0.72MB
- IR API 응답
- 콜드 10.0초→웜 0.001초
- 테스트
- 백엔드 476 · 프런트 281
문제
- HWPX: KS X 6101(OWPML) 표준, ZIP 안에 XML이 든 한글 문서 포맷
- 목표: 브라우저에서 원본과 같은 레이아웃 재현
- 제약
- 참조 구현(한컴오피스)이 블랙박스 → 정답 판정 기준 없음
- 상용 폰트(함초롬·휴먼명조) 부재 → 글자 폭·줄바꿈·페이지 수 변동
- 표준 문서가 기계가독 불가 (HTML 스크레이프)
접근 — 문서 안의 정답지
- 한컴이 저장 시 줄별 레이아웃 좌표를
hp:linesegarray에 기록 → 정답지로 사용 - 세 출처 삼각측량, 어긋나는 지점 = 결함
| 출처 | 단위 변환 | 얻는 것 |
|---|---|---|
| 브라우저 실측 (Playwright) | CSS px × 25.4/96 | 실제로 그려진 위치 |
| 원본 XML | HWPUNIT × 25.4/7200 | 문서가 선언한 값 |
| KS X 6101 표준 | — | 속성의 의미와 적용 조건 |
사례 1 — 용지 여백 해석 오류
- 표준 10.6.6.2/10.6.6.3: 본문·머리말·꼬리말 여백이 쌓이는 4-밴드 구조
- 기존 렌더러: 머리말·꼬리말을 용지 끝 기준으로 해석 → 본문 267mm (20mm 과다)
- 검증: 한컴 캐시의 최심 줄이 4-밴드 한계(247mm)에 0.3~1.2mm까지 근접, 26쪽 중 2-밴드 여유에 닿는 줄 없음
| 문서 | 최심 줄 | 4-밴드 247mm | 2-밴드 267mm |
|---|---|---|---|
| A | 246.7mm | 여유 0.3mm | 여유 20.3mm |
| B | 245.8mm | 여유 1.2mm | 여유 21.2mm |
- 가로축 교차검증: 최대 51026 HWPUNIT = 180.0mm = 210 − 15 − 15
- 부수 효과: 쪽번호가 비인쇄 여백 → 꼬리말 밴드 안으로 이동
사례 2 — CSS float 예약 위치 어긋남
- 증상: 그림 옆 제목이 단어 중간에서 끊기고 문단이 좁은 리본에 눌림
- 실측: 그림 x 132.7–191.6mm, 배제 영역 x 55.8–114.7mm (76.9mm 왼쪽)
- 원인: 같은 변의 두 float 예약이 세로로 겹치면 CSS가 나란히 배치
- 해결:
clear로 세로 스택 강제 → 본문 컬럼 40mm → 117.7mm - 반증된 가설: 밴드 겹침 원인설, post-layout 보정 패스(예약·객체 4.6mm 어긋남 유발)
- 습관: 수정 전 "무엇이 얼마나 바뀌어야 하는가"를 수치로 기록
기타 개선
- CJK 웹폰트
unicode-range분할: 원본 112MB, 문서당 7.6MB → 0.72MB, 페이지 27 → 26쪽 - 캐시:
(mtime_ns, size)키 + 기동 시 워밍 → 콜드 10.0초 → 웜 0.001초, 손상 ZIP은 503·실패 미캐시 - 계측기:
min-height착시 제거(actual − minHeight측정) → 최악 +1,969mm 넘침 발견 - 조용한 폴백 제거: 데이터 소스 배지로 낡은 픽스처 사용 발견
배운 것
- "됐다"를 판정할 방법을 먼저 만들고 고친다
- 참조 구현이 블랙박스여도 산출물 안에 정답이 남아 있을 수 있다
- 표준의 조건절이 곧 테스트 케이스다
- 스펙과 실제 파일이 다르면 파일이 이긴다
- 드러난 결함은
xfail(strict=True)로 고정해 숨기지 않는다
Posts
- 2026-08
문서 목록이 코퍼스 전체를 빌드하고 있었다 — 그리고 그 수정은 절반이었다
AI와 함께 짠 목록 API가 아무도 보지 않는 숫자를 위해 목록 요청마다 코퍼스 전체를 빌드하고 있었다. 명백한 실수를 덮은 뒤 멈추지 않고 계측을 붙여 측정하고 나서야, 그 수정이 절반이었음을 알았다 — 진짜 병목은 이벤트 루프를 막는 동기 IR 빌드였고, 측정만이 그 사실과 다음 수정의 우선순위를 알려줬다.
- 2026-08
측정을 믿으려면 측정 환경부터 배포와 같아야 했다 — 단일 코어와 콜드 풀 재현
앞 포스트에서 측정이 다음 수정의 우선순위를 정해줬지만, 그 우선순위를 믿으려면 측정 환경부터 배포와 같아야 했다. 로컬 부하 스택은 CPU를 덜 조이고 S3 콜드 경로를 아예 안 밟고 있었다. 단일 코어 고정(시나리오 A)과 MinIO 콜드풀 재현을 dep 단위로 하나씩 켜서, H1을 더 선명하게 재확인하고 단일스레드/GIL 가정을 처리량 수치로 갈라 확인했다.