# Pretext: 멀티라인 텍스트 측정과 레이아웃을 위한 TypeScript 라이브러리

> Clean Markdown view of GeekNews topic #28013. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=28013](https://news.hada.io/topic?id=28013)
- GeekNews Markdown: [https://news.hada.io/topic/28013.md](https://news.hada.io/topic/28013.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-03-30T22:35:41+09:00
- Updated: 2026-03-30T22:35:41+09:00
- Original source: [github.com/chenglou](https://github.com/chenglou/pretext)
- Points: 4
- Comments: 1

## Topic Body

- **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](https://chenglou.me/pretext/) 및 [somnai-dreams.github.io/pretext-demos](https://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](https://github.com/chenglou/text-layout) 프로젝트에서 아이디어를 이어받음
- **canvas `measureText` 기반 셰이핑**, **pdf.js의 bidi 처리**, **스트리밍 라인 브레이킹** 설계를 계승해 발전시킨 구조

## Comments



### Comment 54154

- Author: neo
- Created: 2026-03-30T22:35:41+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47556290) 
- 이 프로젝트는 정말 **인상적임**  
  웹페이지에서 텍스트를 실제로 렌더링하지 않고도 줄바꿈된 텍스트의 높이를 효율적으로 계산하는 문제를 해결함  
  단어 단위로 분할된 세그먼트의 **폭과 높이를 캐싱**하고, 브라우저의 줄바꿈 알고리즘을 직접 구현함  
  하이픈, 이모지, 중국어 등 다양한 문자 처리와 브라우저별 렌더링 차이(Safari 포함) 때문에 매우 어려운 작업임  
  실제 브라우저와 비교 테스트를 위해 [corpora 데이터셋](https://github.com/chenglou/pretext/tree/main/corpora)과 [accuracy 테스트 페이지](https://github.com/chenglou/pretext/blob/main/pages/accuracy...)를 사용함
  - 나도 비슷한 걸 만든 적이 있음. **uWrap.js**라는 2KB짜리 단순 버전인데, AI 없이 구현했음  
    ASCII 텍스트 기준으로 내 코드는 80ms, pretext는 2200ms 걸림  
    정확도는 아직 테스트 안 했지만 오늘 밤 해볼 예정임  
    [이슈 #18](https://github.com/chenglou/pretext/issues/18)에 성능 개선 PR들이 이미 열려 있음
  - 텍스트 레이아웃 엔진은 정말 **악명 높게 어려움**  
    나도 예전에 canvas에서 멀티라인 텍스트를 렌더링하려다 고생했음  
    이 프로젝트는 DOM과 직접 연결되어 있어서 훨씬 유용함  
    예시: [Scrawl 데모](https://scrawl-v8.rikweb.org.uk/demo/canvas-206.html)
  - 설명을 보면, 실제로는 세그먼트를 **canvas에 렌더링해서 측정**하는 방식 같음  
    네이티브 API보다 느릴 수 있고, 브라우저의 비-canvas 렌더링과 동일한 로직을 쓴다고 보장할 수 없음
  - 사실상 브라우저의 **텍스트 렌더링 알고리즘을 직접 구현한 건 아님**  
    canvas에 렌더링 후 측정하는 방식이며, 단순히 텍스트 레이아웃 분석용 API를 제공하는 수준임
  - Remotion 비디오용 **동적 자막** 만들 때 텍스트 높이 계산 때문에 고생했는데, 이 라이브러리가 큰 도움이 될 것 같음

- 이건 정말 **오랫동안 기다려온 기능**임  
  예전부터 반응형 아코디언 같은 걸 제대로 구현하기 어려웠음  
  웹 발전 패턴은 항상 ① 복잡한 요구 등장 → ② JS/CSS 해킹 → ③ 표준화로 이어졌음  
  이번엔 해킹이 아닌, 제대로 된 2단계라고 생각함  
  [RESEARCH.md](https://github.com/chenglou/pretext/blob/main/RESEARCH.md)를 보면 브라우저별 이모지 측정 차이까지 세세히 연구했음  
  유지보수는 힘들겠지만, 웹 발전에 큰 **전환점**이 될 것 같음
  - 반응형 아코디언은 이제 CSS로 가능하지만, 여전히 이런 API는 필요했음  
    이번엔 **AI가 개발 과정에 적극 활용**된 게 흥미로움. Cursor 에이전트를 이용해 대부분 구현된 듯함

- 라이브러리 저자에 따르면, Claude Code와 Codex에게 브라우저의 **ground truth 데이터를 학습시켜** 여러 주차에 걸쳐 반복 측정했다고 함  
  [관련 트윗](https://x.com/_chenglou/status/2037715226838343871?s=20) 참고  
  Autoresearch도 일부 사용된 듯함

- shape 기반 리플로우 예제가 특히 마음에 들었음  
  Ensō(enso.sonnet.io)에 적용해보고 싶었지만 단순함을 유지하려고 참았음  
  [아코디언 예제](https://chenglou.me/pretext/accordion)는 CSS의 `interpolate-size`로도 구현 가능함  
  [Josh Comeau의 글](https://www.joshwcomeau.com/snippets/html/interpolate-size/) 참고  
  [텍스트 버블 예제](https://chenglou.me/pretext/bubbles)는 `text-wrap: balance | pretty`로 유사하게 구현 가능함
  - 하지만 `balance`나 `pretty`로는 완전한 해결이 안 됨  
    줄 길이를 균등하게 만드는 건 원하지 않는 경우가 많음  
    관련 CSSWG 이슈: [#191](https://github.com/w3c/csswg-drafts/issues/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 환경에서 데모가 전부 깨져 보임  
  예: [스크린샷](https://files.catbox.moe/4w3um0.png)
  - 이런 종류의 프로젝트는 결국 **끝없는 엣지 케이스 추적**의 연속임을 보여줌

- 이런 기능은 사실 **브라우저 표준 API로 제공**되어야 함  
  W3C에 기능 요청을 하려면 어떻게 해야 하는지, 커뮤니티 투표 같은 게 가능한지 궁금함
  - 이미 [Font Metrics API 제안](https://drafts.css-houdini.org/font-metrics-api-1/)이 존재함  
    하지만 브라우저 벤더들이 우선순위를 두지 않음  
    현재 Chrome은 AI 관련 API에 더 집중하고 있음

- Sciter 엔진에는 이미 [Graphics.Text](https://docs.sciter.com/docs/Graphics/Text) 기능이 있음  
  CSS 스타일을 그대로 적용할 수 있는 **캔버스 기반 텍스트 렌더링 요소**임

- 브라우저의 **텍스트 검색(Ctrl+F)** 이 가상 스크롤 리스트에서는 제대로 작동하지 않음  
  이런 문제를 해결하려면 JS가 아닌 새로운 “Search” API가 필요할지도 모름
  - [Virtual Scroller API](https://wicg.github.io/virtual-scroller/#find-in-page-apis)가 있었지만 거의 진전이 없음  
    관련 프로젝트: [Display Locking](https://github.com/WICG/display-locking), [MDN 문서](https://developer.mozilla.org/en-US/docs/Web/API/Element/bef...)
  - 네이티브 검색은 DOM에 존재하는 노드만 탐색함  
    가상화된 리스트의 오프스크린 항목은 DOM에 없으므로 검색 불가임  
    이를 해결하려면 **선택, 포커스, 스크롤 위치, 매치 탐색**까지 통합된 새로운 브라우저 계약이 필요함  
    하지만 사이트들이 일관되게 사용할 가능성은 낮음
