요약
Pretext.js는 DOM 측정 대신 순수 연산만으로 여러 줄 텍스트의 높이와 배치를 계산하는 zero-dependency TypeScript 라이브러리입니다. 브라우저의 강제 동기 리플로우(reflow)를 제거하고, getBoundingClientRect()보다 최대 500배 빠른 속도로 텍스트 측정이 가능합니다. 전 세계 주요 문자 체계를 모두 지원하므로, 가상 스크롤러, 채팅 UI, 데이터 그리드 등에서 브라우저가 30년간 방치해온 퍼포먼스 병목을 해결할 수 있습니다.
서론
JavaScript가 getBoundingClientRect() 또는 offsetHeight를 호출할 때마다 브라우저는 모든 작업을 중단하고, 보류 중인 스타일을 플러시하며, 전체 레이아웃을 재계산합니다. 이 강제 동기 리플로우는 브라우저가 할 수 있는 작업 중 가장 비용이 많이 듭니다.
예를 들어, 1,000개의 채팅 버블이나 10,000개 행의 데이터 그리드 각각에 이 작업이 반복된다면 프레임 드롭, 버벅임(jank), 사용자 불만족이 필연적으로 발생합니다.
💡 API 기반 프런트엔드를 구축하는 Apidog 팀 역시 이러한 문제를 잘 알고 있습니다. 레이아웃 엔진이 문제를 일으킬 때, 응답 데이터를 동적 UI로 스트리밍하면서 부드러운 UX를 유지하는 것은 현실적인 고민입니다.
Meta의 react-motion 개발자이자 ReasonML/React의 코어 컨트리뷰터인 Cheng Lou가 이 문제를 해결하기 위해 Pretext.js를 만들었습니다. 2026년 3월 출시 후 며칠 만에 14,000+ GitHub 별을 받았고, Hacker News에서도 큰 이슈가 되었습니다.
이 글에서는 Pretext.js의 동작 원리, 사용법, 활용 시점, 한계 등을 실전 코드와 함께 다룹니다.
Pretext.js는 무엇인가요?
Pretext.js는 순수 JavaScript/TypeScript 기반 텍스트 레이아웃 엔진입니다. getBoundingClientRect()나 offsetHeight, 리플로우, 스래싱 없이 연산만으로 여러 줄 텍스트의 레이아웃을 빠르게 측정합니다.
핵심은 브라우저에 "이 텍스트의 높이?"를 물어보는 대신, Canvas API의 폰트 메트릭스를 활용해 수학적으로 답을 구한다는 점입니다.
API는 단 두 가지 함수로 구성됩니다:
import { prepare, layout } from '@chenglou/pretext';
// 1. 텍스트와 폰트로 핸들 준비 (캐시 가능, 1회)
const handle = prepare('Hello, pretext.js', '16px "Inter"');
// 2. 원하는 컨테이너 너비에서 레이아웃 계산 (순수 연산, 매우 빠름)
const { height, lineCount } = layout(handle, 400, 24);
-
prepare(): 텍스트/폰트 조합별로 측정 및 캐시 -
layout(): 캐시된 핸들로 컨테이너 너비/줄높이 기준 레이아웃 및 높이 계산 (DOM/Canvas 접근 없음)
왜 API 기반/가상화 UI에서 중요한가?
AI 채팅, 실시간 대시보드, 협업 에디터 등 스트리밍 API 응답을 쓰는 앱에서는 텍스트가 화면에 렌더링되기 전 높이를 미리 알아야 합니다. 그렇지 않으면 가상 스크롤러가 버벅이고, UI가 갑자기 튀며, 사용자 경험이 나빠집니다.
Pretext.js는 밀리초 대신 마이크로초 단위로 높이 계산이 가능해 대량의 텍스트 처리에서 속도 차이가 크게 누적됩니다.
Pretext.js가 해결하는 문제
강제 동기 리플로우란?
const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
const height = el.getBoundingClientRect().height; // 리플로우 발생!
// height 사용...
});
- 각 호출마다 브라우저가 JS 실행을 멈추고, 스타일 플러시, 전체 레이아웃 재계산, 값 반환
- 1,000개 반복 측정 → 약 94ms 소요(6프레임 손실, 60fps 기준)
가상 스크롤링의 한계
가변 높이 텍스트의 경우, 기존 라이브러리는
- 오프스크린 렌더링 → 측정 → 재배치 (가상화 의미 감소)
- 높이 추정 → 렌더 후 교정(점프 발생)
- 고정 높이 강제(표현력 제한)
Pretext.js는 DOM 생성 전 텍스트 높이를 수학적으로 정확하게 계산합니다.
벤치마크
| 접근 방식 | 1,000개 텍스트 블록 | 500개 텍스트 블록 |
|---|---|---|
DOM (getBoundingClientRect) |
~94ms (6프레임 드롭) | ~47ms |
Pretext.js (layout()) |
~2ms | ~0.09ms |
| 속도 차이 | ~47배 빠름 | ~500배 빠름 |
작은 배치에서는 속도 차이가 더 극적입니다.
Pretext.js의 내부 동작 방식
1. 텍스트 분할(Segmentation)
-
prepare()호출 시, 입력 텍스트 정규화 - UAX #14 등 유니코드 줄바꿈 규칙 적용, 세그먼트 단위 분할
- 다국어(한중일, RTL, 태국, 이모지 등) 및 소프트 하이픈 처리
2. Canvas 측정
- 각 세그먼트를 Canvas
measureText()로 측정(리플로우 없음) - 세그먼트+폰트 조합별 캐시
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello'); // 빠름, reflow 없음
const width = metrics.width;
3. 순수 연산 레이아웃
-
layout()은 캐시된 세그먼트 너비와 컨테이너 너비를 받아 탐욕적 줄바꿈 계산 - 줄 수 계산, 줄 높이 곱해 전체 높이 반환
- DOM/Canvas 추가 접근 없음 (매우 빠름)
재사용 가능한 핸들 패턴
하나의 prepare() 핸들로 여러 너비에서 곧바로 레이아웃 계산:
const handle = prepare(longArticleText, '16px "Inter"');
const mobile = layout(handle, 375, 24);
const tablet = layout(handle, 768, 24);
const desktop = layout(handle, 1200, 24);
반응형 디자인/다양한 뷰포트에 최적.
실전 사용 예시
1. 가변 높이 텍스트 가상 스크롤링
import { prepare, layout } from '@chenglou/pretext';
interface TextItem {
id: string;
content: string;
}
function computeHeights(items: TextItem[], containerWidth: number) {
return items.map(item => {
const handle = prepare(item.content, '14px "Inter"');
const { height } = layout(handle, containerWidth, 20);
return { id: item.id, height: height + 32 }; // 패딩 포함
});
}
const heights = computeHeights(chatMessages, 600);
- 오프스크린 렌더링/보정 점프 없음
2. AI 채팅 스트리밍 인터페이스
let streamedText = '';
const font = '15px "SF Pro"';
socket.on('token', (token: string) => {
streamedText += token;
const handle = prepare(streamedText, font);
const { height } = layout(handle, bubbleWidth, 22);
scroller.updateItemHeight(messageId, height + padding);
});
- 스트리밍 토큰마다 DOM 측정 없이 높이 즉시 갱신
3. 데이터 그리드 열 자동 크기
function computeColumnWidth(values: string[], font: string, padding: number) {
let maxWidth = 0;
for (const value of values) {
const handle = prepare(value, font);
// 무한 너비로 단일 줄 폭 측정
const { height } = layout(handle, Infinity, 20);
// handle에서 내부 폭 추출
maxWidth = Math.max(maxWidth, /* computed width */);
}
return maxWidth + padding;
}
4. 다국어 콘텐츠 피드
const posts = [
{ text: 'This library changed everything', lang: 'en' },
{ text: 'RTL text with correct bidirectional layout', lang: 'ar' },
{ text: 'CJK text gets proper character-level breaks', lang: 'zh' },
];
posts.forEach(post => {
const handle = prepare(post.text, '16px system-ui');
const { height } = layout(handle, 400, 24);
});
- 언어별 줄바꿈 규칙 자동 처리
Apidog로 텍스트 레이아웃 테스트하기
API 기반 UI에서 올바른 텍스트 레이아웃을 구현하려면, API 응답이 올바른 형식/속도/데이터로 공급되는지 테스트하는 것이 중요합니다.
Apidog는 스트리밍 API 응답을 모킹하여 Pretext.js 통합 환경에서 다양한 텍스트 길이, 언어, 유니코드 엣지 케이스로 가상 스크롤러 동작을 검증할 수 있습니다.
AI 챗봇 등에서 Apidog의 API 테스트 환경으로 할 수 있는 것:
- 실제 LLM 출력을 시뮬레이션하는 청크 단위 스트리밍 응답 모킹
- 다국어/엣지 케이스 페이로드로 레이아웃 버그 사전 탐지
- 응답 스키마 유효성 검사로 데이터 포맷 보장
- 자동화된 텍스트 렌더링 엣지 케이스 테스트 스위트 실행
텍스트 레이아웃 라이브러리는 입력 데이터 품질만큼만 정확해집니다. 빠른 측정도 잘못된 API 응답이면 의미 없습니다.
알려진 한계 및 비판
렌더링 정확도 엣지 케이스
- Safari/Chrome 일부 시나리오에서 텍스트가 경계 상자 밖으로 나가는 현상
- 폰트 커닝, 혼합 폰트 크기, 서브픽셀 렌더링, 브라우저별 텍스트 형태 등에서 미세한 차이 발생
Canvas 측정 비용
-
prepare()는 여전히 Canvas 엔진 사용 - 프레임마다 수천개 핸들 생성 시 병목 가능 → 캐싱/배치 처리 필요
CSS 속성 미지원
- 폰트 외의 CSS 속성(
letter-spacing,text-indent,font-feature-settings등) 미반영 - 스타일링이 심한 경우 브라우저 렌더링과 오차 발생
DOM 렌더링 대체 아님
- Pretext.js는 "얼마나 높이?"만 알려줌
- 실제 텍스트 렌더링은 DOM/Canvas/SVG 등 별도 구현 필요
Pretext.js vs 기존 방식
| 특징 | Pretext.js | DOM 측정 | 추정값 |
|---|---|---|---|
| 속도 (1천개) | ~2ms | ~94ms | ~0ms |
| 정확도 | 높음 (Canvas) | 완벽함 | 낮음 |
| DOM 의존성 |
prepare() 이후 없음 |
완전 의존 | 없음 |
| 리플로우 | 없음 | 측정당 1회 | 없음 |
| 다국어 | 완전 지원 | 완전 지원 | 부족 |
| CSS 속성 | 제한적 | 완전 | 없음 |
| 메모리 | 세그먼트 캐시 | DOM 노드 | 최소 |
| 반응형 | 1회 prepare(), 다회 layout()
|
각 너비마다 재측정 | 각 너비마다 재추정 |
- 픽셀 정확도/CSS 스타일이 중요하다면 DOM 실측
- 대량 목록/가상화/속도 위주라면 Pretext.js가 효과적
시작하기
설치
npm install @chenglou/pretext
# 또는
pnpm add @chenglou/pretext
# 또는
bun add @chenglou/pretext
기본 사용 예시
import { prepare, layout } from '@chenglou/pretext';
// 문단 측정
const handle = prepare(
'Pretext.js computes text layout without touching the DOM.',
'16px "Inter"'
);
// 특정 컨테이너 너비에서 높이 계산
const result = layout(handle, 600, 24);
console.log(result.height); // 예: 48 (2줄 x 24px)
console.log(result.lineCount); // 예: 2
React 통합
import { prepare, layout } from '@chenglou/pretext';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMemo, useRef } from 'react';
function VirtualChat({ messages }: { messages: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const containerWidth = 600;
const font = '14px "Inter"';
const lineHeight = 20;
const heights = useMemo(() => {
return messages.map(msg => {
const handle = prepare(msg, font);
const { height } = layout(handle, containerWidth, lineHeight);
return height + 24; // 패딩
});
}, [messages]);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => heights[index],
});
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
width: containerWidth,
}}
>
{messages[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
- 메시지 DOM 렌더 전, 정확한 높이 산출 → 추정/점프/리플로우 없음
인터랙티브 플레이그라운드
Pretext.js 공식 사이트의 pretextjs.dev/playground에서 직접 텍스트 붙여넣기, 폰트 선택, 컨테이너 너비 변경 및 실시간 레이아웃 결과 확인 가능.
Pretext.js를 사용하지 말아야 할 때
- 정적 페이지: 텍스트가 변하지 않고 가상화가 필요 없다면 CSS로 충분
- 픽셀 완벽/인쇄 레이아웃: 서브픽셀 차이가 중요한 상황은 DOM 실측 추천
-
과도한 CSS 텍스트 스타일:
letter-spacing,text-indent등 CSS 속성 의존도가 높으면 오차 발생 - 서버 사이드 렌더링: Node.js에서 Canvas 폴리필 필요, 공식 지원 미완료
- 작고 정적인 목록: 50개 이하라면 DOM 측정도 매우 빠름
자주 묻는 질문
프로덕션 사용 가능성
- 2026년 3월 출시, 빠른 성장
- Midjourney 등 대규모 서비스 운영진이 개발
- 다양한 언어/엣지 케이스 테스트 포함
- 신규 라이브러리이므로 버전 고정/사전 테스트 권장
React/Vue/Svelte 지원 여부
- 프레임워크 무관, 두 함수만 호출하면 됨
웹폰트 처리
-
prepare()호출 시점에 폰트가 로드되어 있어야 측정 결과가 정확 - Font Loading API(
document.fonts.ready) 활용
Canvas/SVG 렌더링 지원
- 렌더 타겟 무관, 측정 결과로 Canvas/WebGL/SVG/DOM에 텍스트 배치 가능
RTL(오른쪽→왼쪽 언어) 지원
- 아랍어, 히브리어 등 양방향 텍스트 완전 지원
번들 크기
- 의존성 없이 15KB(minified), 폴리필 필요 없음
DOM 측정 정확도와 비교
- 대부분 1~2px 내외 오차
- CSS 속성 사용 시 차이 발생 가능
스타일 혼합 텍스트(볼드/이탤릭 등)
- 하나의
prepare()는 단일 폰트만 지원 - 혼합 텍스트는 직접 분할 및 각 스타일별 핸들 생성 필요(향후 개선 예정)
결론
Pretext.js는 브라우저의 고질적 문제였던 "빠르고 정확한 텍스트 측정"을 두 개의 함수로 해결합니다. 대량 가상 스크롤, 채팅 UI, 데이터 그리드 등에서 DOM 리플로우 없이 극한의 속도와 다국어 정확도를 제공합니다.
만능은 아닙니다. CSS 속성 지원 한계, 미세 오차, 서버 미지원 등은 감안해야 합니다. 하지만 가상화된 대량 텍스트 높이 계산에는 최적의 선택지입니다.
더 빠른 텍스트 중심 UI를 만들 준비가 되셨나요? 먼저 Apidog로 API 엔드포인트를 테스트해 데이터 계층을 견고하게 만든 후, Pretext.js를 렌더링 파이프라인에 도입해보세요.


Top comments (0)