Pretext: 멀티라인 텍스트 측정과 레이아웃을 위한 TypeScript 라이브러리
(github.com/chenglou)- Pretext는 DOM 접근 없이 멀티라인 텍스트의 높이와 줄 배치를 계산하는 순수 JavaScript/TypeScript 라이브러리로, 브라우저와 서버 환경 모두 지원
- getBoundingClientRect 같은 DOM 측정 API를 사용하지 않아 레이아웃 리플로우 비용을 제거하고, 폰트 엔진 기반 자체 측정 로직으로 정확도를 확보
- prepare() / layout() API를 통해 텍스트를 전처리하고, 캐시된 폭 데이터를 이용해 순수 산술 연산으로 빠른 높이 계산 수행
- 이모지, 혼합 방향 텍스트(bidi), 다양한 언어를 지원하며, Canvas·SVG·WebGL·서버 렌더링에서도 동일한 결과 제공
- 가상화 스크롤, 텍스트 오버플로 검증, 플로팅 텍스트 배치 등 정밀한 UI 레이아웃 구현에 활용 가능한 고성능 텍스트 엔진임
개요
- Pretext는 멀티라인 텍스트 측정과 레이아웃을 위한 순수 JavaScript/TypeScript 라이브러리로, DOM, Canvas, SVG, 그리고 서버 사이드 렌더링까지 지원
-
DOM 측정 API(
getBoundingClientRect,offsetHeight등)를 사용하지 않아 레이아웃 리플로우 비용을 제거함 - 브라우저의 폰트 엔진을 기준으로 한 자체 측정 로직을 통해 정확하고 빠른 성능을 제공
- 모든 언어, 이모지, 혼합 방향 텍스트(bidi) 를 지원하며, 브라우저별 차이도 처리
설치 및 데모
- 설치:
npm install @chenglou/pretext - 로컬 실행:
bun install후bun start로/demos디렉터리 열기 - 온라인 데모: chenglou.me/pretext 및 somnai-dreams.github.io/pretext-demos
주요 기능
- Pretext는 두 가지 주요 사용 방식을 제공
-
1. DOM 접근 없이 문단 높이 측정
-
prepare()는 텍스트를 전처리하고, 공백 정규화·세그먼트 분리·glue 규칙 적용·canvas 기반 측정을 수행해 불투명 핸들(opaque handle) 을 반환 -
layout()은 캐시된 폭 데이터를 이용해 순수 산술 연산으로 높이와 줄 수 계산 - 동일한 텍스트와 설정에서는
prepare()를 반복 호출하지 않고, 리사이즈 시에는layout()만 다시 실행 -
{ whiteSpace: 'pre-wrap' }옵션으로 공백, 탭(\t), 줄바꿈(\n)을 그대로 유지 - 벤치마크 결과:
prepare()약 19ms (500개 텍스트 기준),layout()약 0.09ms - 반환된 높이 값은 다음과 같은 UI 기능에 활용 가능
- 가상화 및 오클루전 처리에서 정확한 높이 계산
- JS 기반 레이아웃 시스템(예: masonry, flexbox 유사 구조)
- AI 기반 텍스트 오버플로 검증
- 텍스트 로드 시 스크롤 위치 유지
-
-
2. 수동 문단 레이아웃 구성
-
prepareWithSegments()로 세그먼트 단위 데이터 생성 -
layoutWithLines()는 고정 폭에서 각 줄의 텍스트와 폭 정보를 반환 -
walkLineRanges()는 텍스트 문자열을 만들지 않고 각 줄의 폭과 커서 범위를 순회- 예: 여러 폭을 테스트해 적절한 줄 수와 높이를 찾는 이진 탐색형 레이아웃 조정 가능
-
layoutNextLine()은 줄마다 폭이 달라지는 경우 한 줄씩 순차적으로 레이아웃- 예: 이미지 주위로 텍스트를 흐르게 하는 플로팅 텍스트 배치
- 이 방식은 Canvas, SVG, WebGL, 서버 사이드 렌더링에도 동일하게 적용 가능
-
API 요약
-
기본 측정용 API
-
prepare(text, font, options?): 텍스트 분석 및 측정,layout()에 전달할 핸들 반환 -
layout(prepared, maxWidth, lineHeight): 주어진 폭과 줄 높이에 따른 텍스트 높이와 줄 수 계산
-
-
수동 레이아웃용 API
-
prepareWithSegments(text, font, options?): 세그먼트 단위 데이터 반환 -
layoutWithLines(prepared, maxWidth, lineHeight): 각 줄의 텍스트, 폭, 커서 정보 포함 -
walkLineRanges(prepared, maxWidth, onLine): 각 줄의 폭과 커서 범위를 콜백으로 전달 -
layoutNextLine(prepared, start, maxWidth): 줄 단위 반복자 형태로 레이아웃 수행 -
LayoutLine,LayoutLineRange,LayoutCursor타입 정의 포함
-
-
기타 유틸리티
-
clearCache(): 내부 캐시 초기화 -
setLocale(locale?): 로케일 설정 및 캐시 초기화 (기존 상태에는 영향 없음)
-
제약 및 주의사항
- Pretext는 완전한 폰트 렌더링 엔진은 아님
- 기본 대상 CSS 속성
-
white-space: normal -
word-break: normal -
overflow-wrap: break-word -
line-break: auto
-
-
{ whiteSpace: 'pre-wrap' }사용 시 공백, 탭, 줄바꿈 유지하며tab-size: 8적용 - macOS에서
system-ui폰트는layout()정확도에 부적합하므로 명시적 폰트명 사용 권장 -
overflow-wrap: break-word로 인해 매우 좁은 폭에서는 단어 내부에서도 줄바꿈 가능, 단 문자 단위(grapheme) 기준으로만 분리
개발 관련
- 개발 환경 및 명령은
DEVELOPMENT.md참고
기여 및 배경
- Sebastian Markbage의 text-layout 프로젝트에서 아이디어를 이어받음
-
canvas
measureText기반 셰이핑, pdf.js의 bidi 처리, 스트리밍 라인 브레이킹 설계를 계승해 발전시킨 구조
Hacker News 의견들
-
이 프로젝트는 정말 인상적임
웹페이지에서 텍스트를 실제로 렌더링하지 않고도 줄바꿈된 텍스트의 높이를 효율적으로 계산하는 문제를 해결함
단어 단위로 분할된 세그먼트의 폭과 높이를 캐싱하고, 브라우저의 줄바꿈 알고리즘을 직접 구현함
하이픈, 이모지, 중국어 등 다양한 문자 처리와 브라우저별 렌더링 차이(Safari 포함) 때문에 매우 어려운 작업임
실제 브라우저와 비교 테스트를 위해 corpora 데이터셋과 accuracy 테스트 페이지를 사용함- 나도 비슷한 걸 만든 적이 있음. uWrap.js라는 2KB짜리 단순 버전인데, AI 없이 구현했음
ASCII 텍스트 기준으로 내 코드는 80ms, pretext는 2200ms 걸림
정확도는 아직 테스트 안 했지만 오늘 밤 해볼 예정임
이슈 #18에 성능 개선 PR들이 이미 열려 있음 - 텍스트 레이아웃 엔진은 정말 악명 높게 어려움
나도 예전에 canvas에서 멀티라인 텍스트를 렌더링하려다 고생했음
이 프로젝트는 DOM과 직접 연결되어 있어서 훨씬 유용함
예시: Scrawl 데모 - 설명을 보면, 실제로는 세그먼트를 canvas에 렌더링해서 측정하는 방식 같음
네이티브 API보다 느릴 수 있고, 브라우저의 비-canvas 렌더링과 동일한 로직을 쓴다고 보장할 수 없음 - 사실상 브라우저의 텍스트 렌더링 알고리즘을 직접 구현한 건 아님
canvas에 렌더링 후 측정하는 방식이며, 단순히 텍스트 레이아웃 분석용 API를 제공하는 수준임 - Remotion 비디오용 동적 자막 만들 때 텍스트 높이 계산 때문에 고생했는데, 이 라이브러리가 큰 도움이 될 것 같음
- 나도 비슷한 걸 만든 적이 있음. uWrap.js라는 2KB짜리 단순 버전인데, AI 없이 구현했음
-
이건 정말 오랫동안 기다려온 기능임
예전부터 반응형 아코디언 같은 걸 제대로 구현하기 어려웠음
웹 발전 패턴은 항상 ① 복잡한 요구 등장 → ② JS/CSS 해킹 → ③ 표준화로 이어졌음
이번엔 해킹이 아닌, 제대로 된 2단계라고 생각함
RESEARCH.md를 보면 브라우저별 이모지 측정 차이까지 세세히 연구했음
유지보수는 힘들겠지만, 웹 발전에 큰 전환점이 될 것 같음- 반응형 아코디언은 이제 CSS로 가능하지만, 여전히 이런 API는 필요했음
이번엔 AI가 개발 과정에 적극 활용된 게 흥미로움. Cursor 에이전트를 이용해 대부분 구현된 듯함
- 반응형 아코디언은 이제 CSS로 가능하지만, 여전히 이런 API는 필요했음
-
라이브러리 저자에 따르면, Claude Code와 Codex에게 브라우저의 ground truth 데이터를 학습시켜 여러 주차에 걸쳐 반복 측정했다고 함
관련 트윗 참고
Autoresearch도 일부 사용된 듯함 -
shape 기반 리플로우 예제가 특히 마음에 들었음
Ensō(enso.sonnet.io)에 적용해보고 싶었지만 단순함을 유지하려고 참았음
아코디언 예제는 CSS의interpolate-size로도 구현 가능함
Josh Comeau의 글 참고
텍스트 버블 예제는text-wrap: balance | pretty로 유사하게 구현 가능함- 하지만
balance나pretty로는 완전한 해결이 안 됨
줄 길이를 균등하게 만드는 건 원하지 않는 경우가 많음
관련 CSSWG 이슈: #191 -
text-wrap은 줄당 단어 수를 맞추는 데 도움은 되지만, 오른쪽 여백 문제는 여전히 남음
- 하지만
-
pretext는 canvas.measureText를 직접 쓰지 않고, 텍스트와 속성을 JS API로 넘기면 자동으로 레이아웃을 계산해줌
예전엔 measureText를 직접 쓰거나 harfbuzz를 브라우저로 옮겨야 했음
기술적 돌파구라기보다, 기존 요소들을 잘 조합한 결과 같음
다만 Skia-wasm / Canvaskit과의 차이가 궁금함- 차이는 단순함. pretext는 wasm이 아님
- Skia는 거대한 렌더링 엔진임. Flutter처럼 디바이스 독립적 렌더링 API를 제공함
pretext는 AI로 순수 Typescript 기반 글리프 렌더링을 구현한 점이 다름
ffmpeg를 C로 직접 구현한 것과 Dart에서 호출하는 것의 차이처럼 느껴짐
이런 시도가 클라이언트 사이드 FOSS의 새로운 가능성을 보여줌 - 만약 저자가 맞다면, 이건 웹 GUI 프레임워크와 리치 텍스트 에디터에 큰 변화를 줄 것임
-
작년에 HTML로 인쇄용 브로셔 조판 시스템을 만들었는데, 줄바꿈과 고아줄 방지를 위해 Selection API로 박스 경계를 반복 계산했음
지금도 잘 돌아가지만, 이유를 모르는 off-by-one 해킹이 있음
pretext의 반복적 줄 생성 기능은 정말 반가운 기능임 -
Fedora + Firefox 환경에서 데모가 전부 깨져 보임
예: 스크린샷- 이런 종류의 프로젝트는 결국 끝없는 엣지 케이스 추적의 연속임을 보여줌
-
이런 기능은 사실 브라우저 표준 API로 제공되어야 함
W3C에 기능 요청을 하려면 어떻게 해야 하는지, 커뮤니티 투표 같은 게 가능한지 궁금함- 이미 Font Metrics API 제안이 존재함
하지만 브라우저 벤더들이 우선순위를 두지 않음
현재 Chrome은 AI 관련 API에 더 집중하고 있음
- 이미 Font Metrics API 제안이 존재함
-
Sciter 엔진에는 이미 Graphics.Text 기능이 있음
CSS 스타일을 그대로 적용할 수 있는 캔버스 기반 텍스트 렌더링 요소임 -
브라우저의 텍스트 검색(Ctrl+F) 이 가상 스크롤 리스트에서는 제대로 작동하지 않음
이런 문제를 해결하려면 JS가 아닌 새로운 “Search” API가 필요할지도 모름-
Virtual Scroller API가 있었지만 거의 진전이 없음
관련 프로젝트: Display Locking, MDN 문서 - 네이티브 검색은 DOM에 존재하는 노드만 탐색함
가상화된 리스트의 오프스크린 항목은 DOM에 없으므로 검색 불가임
이를 해결하려면 선택, 포커스, 스크롤 위치, 매치 탐색까지 통합된 새로운 브라우저 계약이 필요함
하지만 사이트들이 일관되게 사용할 가능성은 낮음
-
Virtual Scroller API가 있었지만 거의 진전이 없음