Pretext를 활용한 텍스트 측정
Pretext는 Cheng Lou가 만든 텍스트 측정 및 레이아웃 라이브러리입니다. 스크롤, 범위 계산, 항목 배치, 특정 위치로 스크롤하는 동작은 계속 TanStack Virtual이 담당하며, 높이가 주로 줄 바꿈된 텍스트에 의해 결정되는 행의 텍스트 높이 추정은 Pretext가 담당할 수 있습니다.
DOM 측정으로 인해 눈에 보이는 위치 보정이 발생하는 채팅 로그, AI 스트림, 활동 피드, 댓글, 변경 로그, 알림 및 기타 텍스트 중심 타임라인에 유용합니다.
사용 시점
각 가상 행의 높이를 다음 항목에서 도출할 수 있다면 Pretext를 사용합니다.
- 텍스트 콘텐츠
- 렌더링된 텍스트에 사용된 정확한 canvas 글꼴 문자열
- 사용 가능한 콘텐츠 너비
- 렌더링된 줄 높이
- 렌더링 결과와 일치하는 공백, 단어 줄 바꿈 및 자간 설정
높이가 이미지, 임베드, 블록 Markdown, 로드된 컴포넌트 또는 임의의 CSS 레이아웃에 따라 달라지는 행을 Pretext가 담당하게 하지 마십시오. 이러한 행에는 measureElement를 사용하거나, 추가 콘텐츠가 확정될 때 resizeItem을 호출하거나, 텍스트 전용 부분과 텍스트가 아닌 부분을 별도의 크기 계산 경로로 분리합니다.
설치
npm install @chenglou/pretext
기본 패턴
텍스트 및 텍스트 스타일 입력을 기준으로 prepare() 결과를 캐시합니다. 현재 너비에 대해 layout()을 실행합니다. 너비, 글꼴, 줄 높이 또는 텍스트 옵션이 변경되면 새 추정값으로 오프셋을 다시 계산하도록 Virtual의 측정값을 재설정합니다.
import { clearCache, layout, prepare } from '@chenglou/pretext'
import { useVirtualizer } from '@tanstack/react-virtual'
const font = '14px Arial'
const lineHeight = 20
const preparedCache = new Map<string, ReturnType<typeof prepare>>()
function getPrepared(row: { id: string; text: string }) {
const key = `${row.id}:${font}:${row.text}`
const cached = preparedCache.get(key)
if (cached) {
return cached
}
const prepared = prepare(row.text, font, {
whiteSpace: 'pre-wrap',
letterSpacing: 0,
})
preparedCache.set(key, prepared)
return prepared
}
function estimateRowHeight(row: { id: string; text: string }, contentWidth: number) {
const text = layout(getPrepared(row), contentWidth, lineHeight)
const textHeight = Math.max(lineHeight, text.height)
return textHeight + 24
}
function Messages({ rows }: { rows: Array<{ id: string; text: string }> }) {
const parentRef = React.useRef<HTMLDivElement>(null)
const [width, setWidth] = React.useState(640)
React.useLayoutEffect(() => {
const element = parentRef.current
if (!element) {
return
}
const update = () => setWidth(element.clientWidth)
const observer = new ResizeObserver(update)
update()
observer.observe(element)
return () => observer.disconnect()
}, [])
const virtualizer = useVirtualizer({
count: rows.length,
getItemKey: (index) => rows[index]!.id,
getScrollElement: () => parentRef.current,
estimateSize: (index) => estimateRowHeight(rows[index]!, width - 32),
})
React.useLayoutEffect(() => {
virtualizer.measure()
}, [virtualizer, width])
React.useEffect(() => {
document.fonts.ready.then(() => {
preparedCache.clear()
clearCache()
virtualizer.measure()
})
}, [virtualizer])
return <div ref={parentRef}>{/* render virtual rows */}</div>
}
견고성 체크리스트
- CSS와 Pretext 입력을 정확히 일치시킵니다.
font,line-height,letter-spacing,white-space,word-break는 렌더링된 행과 일치해야 합니다. - 이름이 지정된 글꼴을 사용하는 편이 좋습니다. 특히 macOS에서는 시스템 글꼴 별칭이 CSS와 canvas에서 서로 다르게 매핑될 수 있습니다.
- 캐시된 측정값을 신뢰하기 전에 글꼴이 준비될 때까지 기다립니다.
document.fonts.ready이후에 준비된 텍스트 캐시를 비우고, Pretext의clearCache()를 호출한 다음,virtualizer.measure()를 호출합니다. - 크기가 조정될 때
layout()을 다시 실행하고prepare()는 실행하지 않습니다.prepare()는 텍스트마다 실행하는 비용이 큰 설정 작업이며,layout()은 너비에 따라 달라지는 비용이 적은 경로입니다. - UI가 빈 행을 한 줄로 렌더링한다면 빈 텍스트에 최솟값을 적용합니다. Pretext는 빈 문자열에 대해 높이 0을 반환합니다.
- 행마다 크기를 결정하는 주체를 하나만 사용합니다. DOM 측정값이 텍스트 추정값을 재정의하도록 의도한 경우가 아니라면, 동일한 행에
measureElement를 호출하면서resizeItem또는 Pretext 추정값으로 크기를 지정하지 마십시오. - 지원되지 않는 런타임을 위한 대체 방식을 유지합니다. 현재 Pretext에는
Intl.Segmenter와 Canvas 2D 텍스트 측정이 필요합니다. - Markdown 전처리, 이미지 메타데이터 로드 또는 제어된 펼치기/접기 전환이 끝난 뒤처럼 렌더링 외부에서 행의 최종 크기를 알게 되면
resizeItem(index, size)를 사용합니다.
완전한 채팅 스타일 구현은 React Pretext 예제를 참조합니다. React Pretext