GitHub - chenglou/pretext
핵심 포인트
- 1Pretext는 DOM `layout reflow`를 유발하는 비싼 브라우저 측정 대신 자체 텍스트 측정 로직을 구현하여 다중선 텍스트의 빠르고 정확한 측정 및 레이아웃을 제공하는 JavaScript/TypeScript 라이브러리입니다.
- 2이 라이브러리는 Canvas의 폰트 엔진을 사용하여 텍스트를 미리 측정하고 캐시된 값을 통해 `layout`을 계산하며, 특정 텍스트의 높이와 줄 수를 얻거나 각 줄을 수동으로 제어하여 Canvas, SVG 등 다양한 환경에 렌더링할 수 있도록 합니다.
- 3Pretext는 모든 언어와 이모지, 혼합 bidi 텍스트를 지원하며, 정확한 텍스트 크기 계산을 통해 웹 UI의 가상화, 사용자 정의 레이아웃, `layout shift` 방지 등 고급 기능을 가능하게 합니다.
Pretext는 multiline text의 측정 및 레이아웃을 위한 Pure JavaScript/TypeScript 라이브러리입니다. 이 라이브러리의 핵심 목표는 브라우저의 DOM 측정 API(예: getBoundingClientRect, offsetHeight) 사용을 회피하여, 브라우저 레이아웃 리플로우(reflow)와 같은 값비싼 작업을 방지하는 것입니다. 이를 위해 Pretext는 자체적인 텍스트 측정 로직을 구현하며, 브라우저의 폰트 엔진을 canvas 기반으로 활용하여 정확한 측정값을 얻습니다.
핵심 방법론은 다음과 같은 두 가지 주요 사용 사례를 지원하는 API 디자인으로 구현됩니다:
- 단락의 높이를 DOM 접근 없이 측정:
prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedText: 이 함수는 텍스트를 분석하고 전처리하는 일회성 작업(one-time text analysis)을 수행합니다. 구체적으로, 입력된 텍스트의 공백을 정규화하고, 텍스트를 세그먼트로 분할하며, 글루 규칙(glue rules)을 적용합니다. 이후 각 세그먼트의 너비를 Canvas API를 사용하여 측정하고 캐시합니다. 이 과정은 상대적으로 비용이 많이 들지만(예: 500개 텍스트 배치에 약 19ms), 한 번만 실행하면 됩니다. 결과로 불투명한 핸들인PreparedText객체를 반환합니다.font매개변수는 CSSfont선언과 동일한 형식(예:'16px Inter')이어야 하며,whiteSpace옵션으로pre-wrap을 설정하면 일반 공백, 탭, 하드 브레이크(\n)가 보존됩니다.layout(prepared: PreparedText, maxWidth: number, lineHeight: number) : { height: number, lineCount: number }:prepare()를 통해 얻은PreparedText핸들, 최대 너비(maxWidth), 줄 높이(lineHeight)를 인자로 받아 텍스트의 최종 높이와 줄 수를 계산합니다. 이 함수는prepare()에서 캐시된 너비 데이터를 기반으로 순수 산술 연산(pure arithmetic)만을 수행하기 때문에 매우 빠릅니다(예: 동일한 500개 텍스트 배치에 약 0.09ms). 따라서 창 크기 변경과 같은 경우에는prepare()를 다시 실행할 필요 없이layout()만 호출하여 비용을 절감합니다.
- 단락 줄을 수동으로 레이아웃:
prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedTextWithSegments:prepare()와 유사하지만, 수동 레이아웃에 필요한 더 풍부한 구조의PreparedTextWithSegments객체를 반환합니다.layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number) : { height: number, lineCount: number, lines: LayoutLine[] }:layout()과 비슷하게 높이와 줄 수를 반환하지만, 추가적으로 각 줄에 대한 상세 정보(LayoutLine배열)를 포함합니다.LayoutLine타입은{ text: string, width: number, start: LayoutCursor, end: LayoutCursor }로 구성됩니다.- : 이는 저수준 API로, 실제 텍스트 문자열을 구성하지 않고 각 줄의 너비와 시작/끝 커서(
LayoutCursor) 정보만을 제공합니다.onLine콜백 함수를 각 줄에 대해 한 번씩 호출하며, 특정 너비에서 텍스트가 어떻게 분할되는지 추측적으로 테스트하는 데 유용합니다 (예: 이진 탐색을 통해 최적의 너비를 찾을 때).LayoutLineRange타입은{ width: number, start: LayoutCursor, end: LayoutCursor }로 정의됩니다. layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number) : LayoutLine | null: 이터레이터(iterator) 방식의 API로, 한 번에 한 줄씩 텍스트를 레이아웃할 수 있습니다. 각 줄의maxWidth를 다르게 설정할 수 있어, 예를 들어 이미지 주위로 텍스트를 흐르게 하는 등의 복잡한 레이아웃에 활용됩니다. 이전 줄의end커서를 다음start커서로 전달하여 텍스트 전체를 반복할 수 있습니다.LayoutCursor는{ segmentIndex: number, graphemeIndex: number }로, 세그먼트와 그 안의 grapheme 인덱스를 나타냅니다.
Pretext는 white-space: normal, word-break: normal, overflow-wrap: break-word, line-break: auto와 같은 일반적인 CSS 텍스트 속성을 기본값으로 가정하며, 이들 속성과 일치하도록 텍스트 레이아웃을 처리합니다. 이를 통해 웹 UI에서 가상화(virtualization), 오클루전(occlusion), 사용자 정의 레이아웃, 레이아웃 쉬프트 방지 등 다양한 고급 기능을 정확하고 효율적으로 구현할 수 있게 합니다. 내부적으로는 텍스트 측정 및 레이아웃을 위해 Sebastian Markbage의 text-layout 개념(canvas measureText를 통한 셰이핑, pdf.js에서 영감받은 BIDI 처리, 스트리밍 줄바꿈)을 활용합니다.