웹 개발 30년 역사상 가장 성가신 문제 중 하나가 바로 "텍스트 높이 측정"이었습니다.
매번 DOM을 조작하고 getBoundingClientRect()를 호출해야 했던 시대는 이제 끝났습니다.
Midjourney 엔지니어이자 React Motion의 창시자인 Cheng Lou가 선보인 Pretext는
DOM 측정 없이 300배 빠른 텍스트 레이아웃을 가능하게 하는 혁신적인 JavaScript 라이브러리입니다.
이 글에서 Pretext의 작동 원리, 실제 활용법, 그리고 커뮤니티의 뜨거운 반응까지 심층 분석합니다.
30년 된 웹의 고질적 문제: 텍스트 측정의 딜레마 🎯
웹 개발자라면 누구나 한 번쯤 겪어봤을 문제가 있습니다. "이 텍스트가 300px 너비에서 몇 줄로 줄바꿈되고, 총 높이는 얼마나 될까?" 이 단순해 보이는 질문에 답하기 위해 우리는 수십 년간 DOM 조작이라는 무거운 대가를 치러왔습니다.
🔴 기존 방식의 문제점
전통적인 텍스트 측정 방식은 다음과 같은 단계를 거칩니다:
- 보이지 않는 DOM 요소 생성
- 스타일 설정 (font, size, width 제약)
- 텍스트 삽입
getBoundingClientRect()또는offsetHeight호출- 브라우저가 전체 레이아웃 리플로우(Reflow) 트리거
- 결과 반환
문제는 한 번의 측정이 전체 문서의 레이아웃을 재계산하게 만든다는 점입니다. 500개의 텍스트 블록을 측정하면 500번의 리플로우가 발생하고, 이는 메인 스레드를 블로킹하여 UI가 멈추는(jank) 현상을 유발합니다.
Google의 Chrome DevTools 팀에 따르면, 강제 리플로우 하나가 모바일 기기에서 10-100ms의 메인 스레드 블로킹을 유발할 수 있습니다. 이를 500개의 텍스트 블록에 곱하면 순식간에 초 단위의 지연이 발생합니다. 특히 가상 스크롤링(virtual scrolling)이나 masonry 레이아웃처럼 동적인 UI에서는 이 문제가 치명적입니다.
"Layout reflow is the #1 cause of jank on the web."
— Google Lighthouse Audit
Pretext의 등장: DOM을 건너뛰는 혁명 ⚡
2026년 3월, Midjourney의 수석 엔지니어이자 React Motion(21,700+ GitHub stars)의 창시자인 Cheng Lou가 Pretext를 공개했습니다. 이 15KB짜리 순수 JavaScript/TypeScript 라이브러리는 단순한 성능 개선을 넘어 패러다임 자체를 바꾸는 도구입니다.
Pretext의 핵심 인사이트는 놀랍도록 단순합니다:
canvas.measureText()는 DOM 렌더링과 동일한 폰트 엔진을 사용하지만
레이아웃 리플로우를 트리거하지 않는다는 점을 이용하는 것입니다.
Canvas는 레이아웃 트리 완전히 외부에서 동작하는 독립적인 컨텍스트입니다.
전 언어 지원
영어, 한국어, 중국어, 아랍어, RTL 언어, 이모지까지 완벽 지원합니다. Intl.Segmenter를 활용한 지능적인 텍스트 분절이 핵심입니다.
제로 리플로우
DOM 측정이 전혀 없어 레이아웃 스래싱(layout thrashing)이 완전히 제거됩니다. 순수 산술 연산만으로 텍스트 레이아웃을 계산합니다.
다중 렌더링
DOM, Canvas, SVG, WebGL, 그리고 곧 서버 사이드 렌더링까지 지원합니다. 동일한 코드로 어디서나 텍스트를 레이아웃할 수 있습니다.
AI 친화적
브라우저의 실제 렌더링을 "ground truth"로 삼아 Claude Code와 Codex가 수 주간 측정과 반복을 통해 정확도를 극대화했습니다.
핵심 아키텍처: prepare()와 layout()의 두 단계 🏗️
Pretext의 아키텍처는 두 단계로 명확히 분리되어 있습니다. 이는 한 번의 비용(Prepare)을 지불하고 무한히 재사용(Layout)할 수 있는 캐싱 전략의 정수입니다.
Phase 1: prepare() - 한 번의 투자
prepare() 함수는 텍스트가 처음 나타날 때 한 번만 실행되는
"비싼" 단계입니다. 다음 작업을 수행합니다:
- Intl.Segmenter를 사용한 지능적인 텍스트 분절
- Canvas measureText()로 각 세그먼트의 너비 측정
- 결과 캐싱 (동일 텍스트/폰트 재사용 가능)
- 양방향 텍스트(BiDi) 처리
- 이모지 및 특수 문자 보정
import { prepare, layout } from '@chenglou/pretext'
// Phase 1: 한 번만 실행 (약 19ms for 500 texts)
const prepared = prepare(
'AGI 春天到了. بدأت الرحلة 🚀',
'16px Inter'
)
// Phase 2: 리사이즈 등 변경 시마다 실행 (약 0.09ms)
const { height, lineCount } = layout(prepared, textWidth, 20)
// 순수 산술 연산만 수행 - DOM 리플로우 ZERO!
Phase 2: layout() - 무한한 재사용
layout() 함수는 캐시된 너비 데이터를 기반으로
순수 산술 연산만 수행합니다. 폰트 크기나 컨테이너 너비가 변경될 때마다
DOM을 건드리지 않고 즉시 새 레이아웃을 계산할 수 있습니다.
💡 핵심 포인트
prepare()는 텍스트가 변경될 때만,
layout()은 컨테이너 크기가 변경될 때만 호출하세요.
이 두 단계의 분리가 Pretext의 성능 비밀입니다.
고급 API: 수동 라인 레이아웃
더 세밀한 제어가 필요하다면 prepareWithSegments와 함께
다음 API들을 사용할 수 있습니다:
import { prepareWithSegments, layoutWithLines, walkLineRanges, layoutNextLine } from '@chenglou/pretext'
// 모든 라인 정보 가져오기
const prepared = prepareWithSegments('텍스트', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26)
// 라인별 너비와 커서 위치만 확인 (문자열 생성 없음)
walkLineRanges(prepared, 320, line => {
console.log(line.width, line.start, line.end)
})
// 동적 너비 - 이미지 주변 텍스트 흐름
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
const width = y < image.bottom ? columnWidth - image.width : columnWidth
const line = layoutNextLine(prepared, cursor, width)
if (line === null) break
ctx.fillText(line.text, 0, y)
cursor = line.end
y += 26
}
성능 비교: 300배 빠르다는 것의 의미 📊
Pretext의 성능 향상은 단순한 숫자가 아닙니다. 300-600배의 속도 향상은 웹 UI 개발의 가능성 자체를 바꿉니다.
| 지표 | DOM 측정 | Pretext |
|---|---|---|
| 500개 텍스트 처리 시간 | 15-30ms + 500회 리플로우 | 0.09ms + 0회 리플로우 |
| 속도 비율 | 1x (기준) | 300-600x 빠름 |
| 리플로우 발생 | 500회 | 0회 |
| 메인 스레드 블로킹 | 동기적 (Yes) | 없음 (No) |
| 의존성 | 전체 DOM | Canvas context only |
| 번들 크기 | N/A | 15KB (gzipped: ~5KB) |
🎯 실제 의미
60fps를 유지하려면 각 프레임이 16.67ms 이내에 완료되어야 합니다. 기존 DOM 측정만으로도 15-30ms가 소모되어 렌더링할 시간이 전혀 없었습니다. Pretext는 0.09ms만으로 레이아웃 계산을 끝내고, 나머지 16.58ms를 시각적 효과에 사용할 수 있게 합니다.
이는 드래곤이 움직이면서 텍스트가 실시간으로 흐름을 바꾸는 데모가 가능한 이유입니다. 매 프레임마다 텍스트 레이아웃을 재계산하면서도 60fps를 유지할 수 있습니다.
브라우저별 성능 차이
Pretext는 브라우저별 특성을 고려하여 최적화되어 있습니다. 특히 Safari에서 극적인 성능 향상을 보입니다:
- Chrome: layout() 0.09ms vs DOM 43.50ms → 483배 빠름
- Safari: layout() 0.12ms vs DOM 149.00ms → 1,242배 빠름
실전 활용: 코드 예제와 구현 패턴 💻
설치
npm install @chenglou/pretext
# 또는
bun add @chenglou/pretext
사용 사례 1: 가상 스크롤링 (Virtual Scrolling)
100,000개의 행을 가진 테이블에서 각 행의 높이를 미리 계산해야 할 때:
import { prepare } from '@chenglou/pretext'
// 모든 행의 텍스트를 미리 prepare
const preparedRows = rows.map(row => ({
id: row.id,
prepared: prepare(row.text, '14px Inter')
}))
// 스크롤 위치에 따라 필요한 행의 높이만 계산
function getRowHeight(rowId, containerWidth) {
const row = preparedRows.find(r => r.id === rowId)
const { height } = layout(row.prepared, containerWidth, 20)
return height
}
사용 사례 2: Masonry 레이아웃
Pinterest 스타일의 masonry 그리드에서 각 카드의 높이를 예측:
function calculateMasonryLayout(items, containerWidth, columnCount) {
const columnHeights = new Array(columnCount).fill(0)
const positions = []
items.forEach(item => {
const { height } = layout(item.prepared,
containerWidth / columnCount - GAP,
LINE_HEIGHT
)
// 가장 짧은 열에 배치
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights))
positions.push({
x: shortestColumn * (containerWidth / columnCount),
y: columnHeights[shortestColumn]
})
columnHeights[shortestColumn] += height + GAP
})
return positions
}
사용 사례 3: 접근 가능한 아코디언
접기/펼치기 애니메이션에서 컨텐츠의 정확한 높이를 미리 알아야 할 때:
function Accordion({ title, content }) {
const [isOpen, setIsOpen] = useState(false)
const prepared = useMemo(() =>
prepare(content, '16px Inter'),
[content]
)
// DOM을 렌더링하기 전에 높이 계산 가능!
const { height } = layout(prepared, containerWidth, 24)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>{title}</button>
<div style={{
height: isOpen ? height : 0,
transition: 'height 0.3s ease',
overflow: 'hidden'
}}>
{content}
</div>
</div>
)
}
사용 사례 4: 텍스트 영역 자동 높이 조절
function AutoResizeTextarea() {
const [value, setValue] = useState('')
// textarea 스타일의 공백 처리
const prepared = useMemo(() =>
prepare(value, '16px Inter', { whiteSpace: 'pre-wrap' }),
[value]
)
const { height } = layout(prepared, textareaWidth, 24)
return (
<textarea
value={value}
onChange={e => setValue(e.target.value)}
style={{ height: `${height}px`, resize: 'none' }}
/>
)
}
데모 분석: 드래곤부터 에디토리얼 엔진까지 🎨
Pretext의 데모 페이지(chenglou.me/pretext)는 단순한 기술 데모가 아닌, 실제로 가능한 UI 패턴의 스펙트럼을 보여줍니다.
Dragon
80개의 세그먼트로 구성된 드래곤 주변으로 텍스트가 흐릅니다. 드래곤이 움직일 때마다 텍스트 레이아웃이 실시간으로 재계산됩니다.
60fps 유지Bubbles
최소한의 공간을 차지하는 멀티라인 메시지 버블. "shrink wrap" 기능으로 30년간 웹에서 불가능했던 레이아웃을 구현합니다.
CSS 미지원 기능Editorial Engine
잡지 스타일의 멀티컬럼 레이아웃. 애니메이션 오브젝트가 텍스트를 밀어내는 동적 플로우를 보여줍니다.
전문가용Justification Comparison
CSS justification, greedy hyphenation, Knuth-Plass 단락 레이아웃을 나란히 비교하여 리버(river) 현상과 간격 트레이드오프를 시각화합니다.
타이포그래피Variable Typographic ASCII
가변 폰트를 활용한 ASCII 아트. 비례 글리프와 모노스페이스 버전을 비교합니다.
예술적Masonry
텍스트 카드 가상화 데모. DOM 측정 없이 높이를 예측하여 occlusion을 구현합니다.
실용적
"The community has spent three days building dragons.
It should be building chat interfaces."
— Dev.to 커뮤니티
이 데모들이 보여주는 핵심은 Pretext가 단순히 빠른 것이 아니라 이전에는 불가능했던 UI 패턴을 가능하게 한다는 점입니다. 특히 "shrink wrap" 기능(멀티라인 텍스트의 최소 너비 계산)은 CSS가 30년간 지원하지 않았던 기능입니다.
커뮤니티 반응과 미래 전망 🌟
Pretext는 공개된 지 48시간 만에 GitHub 스타 14,000개, X(트위터) 조회수 1,900만 회를 기록하며 폭발적인 반응을 얻었습니다. 현재는 20,000개 이상의 스타를 보유하고 있습니다.
Hacker News 커뮤니티 반응
"This thing is very impressive. The problem it solves is efficiently calculating the height of some wrapped text on a web page, without actually rendering that text to the page first (very expensive)."
"Text layout engines are stupidly hard. You start out thinking 'It's a hard task, but I can do it' and then 3 months later you find yourself in a corner screaming 'Why, Chinese? Why do you need to rotate your punctuation differently when you render in columns??'"
개발자들의 실제 활용 사례
- Remotion 사용자: 동적 자막 생성 시 텍스트 줄 수 계산의 어려움을 해결
- Recharts: 차트의 텍스트 레이블 최적화에 Pretext 고려 중
- AI 에이전트 개발자: Claude Code와 Codex를 활용한 개발 워크플로우에 적합
- 접근성 전문가: 스크린 리더 호환성을 유지하면서 성능 개선 가능
향후 로드맵
Cheng Lou는 다음 기능들을 계획 중이라고 밝혔습니다:
- 서버 사이드 렌더링 지원 (Node.js 환경에서 Canvas 없이 동작)
- 더 많은 CSS 속성 지원 (letter-spacing, word-spacing 등)
- Web Worker 호환성 개선
- 더 많은 언어 특화 처리 (한국어 조사 처리 등)
⚠️ 주의사항
Pretext는 white-space: normal, word-break: normal,
overflow-wrap: break-word, line-break: auto를
기본으로 합니다. macOS에서는 system-ui 폰트 대신
명시적인 폰트 이름을 사용해야 정확도가 보장됩니다.
결론: 웹 개발의 새로운 표준 🎯
Pretext는 단순한 라이브러리가 아닙니다. 그것은 30년간 웹 개발자들이 받아들여야만 했던 제약을 해제하는 패러다임의 전환점입니다.
🚀 Pretext가 바꾸는 것들
- 가상 스크롤링: 정확한 높이 예측으로 깜빡임 없는 스크롤
- Masonry 레이아웃: DOM 측정 없이 카드 높이 예측
- 채팅 인터페이스: 실시간 메시지 버블 레이아웃
- 개발 시간 검증: 버튼 라벨이 다음 줄로 넘어가는지 브라우저 없이 확인
- 레이아웃 시프트 방지: 새 텍스트 로드 시 스크롤 위치 재조정
- AI 생성 UI: LLM이 생성한 텍스트의 레이아웃을 실시간으로 계산
React Motion으로 애니메이션의 패러다임을 바꿨던 Cheng Lou가 다시 한 번 웹 개발의 지형을 바꾸고 있습니다. 15KB라는 작은 크기에 담긴 이 엔진은 앞으로 수년간 고성능 텍스트 UI의 표준이 될 것입니다.
"The engine's tiny (few kbs), aware of browser quirks, supports all the languages
you'll need, including Korean mixed with RTL Arabic and platform-specific emojis.
This was achieved through showing Claude Code and Codex the browsers ground truth,
and have them measure & iterate against those at every significant container width,
running over weeks."
— Cheng Lou
지금 바로 npm install @chenglou/pretext를 실행해보세요.
DOM 리플로우의 속박에서 벗어나, 순수 산술의 자유로운 세계로 들어설 준비가 되셨나요?
텍스트 레이아웃의 미래는 이미 여기에 있습니다. 🚀