본문으로 건너뛰기

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