TextMeasurer는 Jetpack Compose의 API로, 화면에 실제로 렌더링되기 전에 텍스트의 크기를 측정하도록 설계되었습니다. Android View의 Paint.measureText를 통한 클래식 접근 방식과 달리 TextMeasurer는 스타일과 제약 조건을 고려하여 너비, 높이 및 줄 메트릭을 얻는 Compose 네이티브 방법을 제공합니다. Android Developers Documentation(2025)에 따르면 TextMeasurer는 텍스트 크기가 다른 요소의 위치 지정에 영향을 미치는 커스텀 Layout 컴포넌트, 애니메이션 레이블 및 적응형 UI 요소에서 적극적으로 사용됩니다.
핵심 요점
TextMeasurer는 androidx.compose.ui.text 패키지의 기능 인터페이스로, 화면에 실제로 렌더링하지 않고 Compose에서 텍스트를 측정할 수 있게 합니다. 이는 Compose 텍스트 엔진의 일부이며 Skia를 사용하여 렌더링하는 Paragraph API 수준에서 작동합니다.
TextMeasurer의 주요 목적은 사전 측정입니다. Compose에서 텍스트 크기는 일반적으로 Composable이 Layout에 배치된 후에 결정됩니다. 그러나 레이아웃 전에 텍스트 너비를 알아야 하는 시나리오가 있습니다. 예를 들어, 문자열이 컨테이너에 맞는지 결정하거나 텍스트 길이에 따라 버튼 크기를 계산하는 경우입니다.
TextMeasurer는 measure() 메서드를 제공하여 이 문제를 해결합니다. 이 메서드는 TextMeasureRequest(텍스트, 스타일, 제약 조건)를 받고 TextLayoutResult(메트릭, 줄, 문자)를 반환합니다. 이를 통해 개발자는 한 프레임 지연 없이 모든 텍스트 데이터를 동기식으로 얻을 수 있습니다.
텍스트 크기가 부모 컴포넌트 크기에 영향을 미칠 때, 애니메이션 텍스트 전환을 만들 때, 또는 텍스트가 다른 요소와 공존하는 커스텀 Layout을 구현할 때 TextMeasurer를 사용하세요.
TextMeasurer API는 두 가지 주요 엔터티로 구성됩니다: rememberTextMeasurer()(인스턴스 생성)와 measure()(측정 실행). 인스턴스는 전체 컴포넌트 수명 주기 동안 한 번 생성되고 모든 측정에 재사용됩니다.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// result.size.width, result.size.height, result.lineCount
constraints 매개변수는 텍스트가 맞아야 하는 최대 너비와 높이를 정의합니다. 텍스트가 너비에 맞지 않으면 다음 줄로 줄바꿈됩니다. 측정 결과에는 TextLayoutResult가 포함되며, 줄 수, 각 문자의 위치, 각 줄의 너비 및 전체 텍스트 크기를 제공합니다.
TextMeasurer는 UI 스레드에서 동기식으로 작동하며 재구성을 트리거하지 않습니다. 그러나 다른 스타일로 자주 텍스트를 측정하면 오버헤드가 발생할 수 있으므로 인스턴스는 remember를 통해 생성되고 재사용됩니다.
가장 간단한 시나리오는 부모 컨테이너의 크기를 결정하기 위해 한 줄 텍스트의 너비를 측정하는 것입니다. 예를 들어, 레이블 길이에 따라 너비가 결정되는 버튼의 경우입니다.
val textMeasurer = rememberTextMeasurer()
var label by remember { mutableStateOf("Submit") }
val textResult = remember(label) {
textMeasurer.measure(
text = label,
style = MaterialTheme.typography.labelLarge,
constraints = Constraints(maxWidth = 1000, maxHeight = 100)
)
}
Box(
modifier = Modifier
.width(textResult.size.width.dp)
.height(40.dp)
.background(Color.Blue)
.clickable { onClick() }
) {
Text(text = label, style = MaterialTheme.typography.labelLarge)
}
이 예제에서 remember(label)은 텍스트가 변경될 때만 측정이 다시 시작되도록 보장합니다. 제약 조건은 줄바꿈을 방지하기 위해 큰 maxWidth(1000)로 설정됩니다. 실제 제약 조건은 텍스트 너비와 같습니다. Box 너비는 측정된 텍스트 너비로 설정되어 “텍스트에 맞는 버튼” 효과를 만듭니다.
참고: 결과의 너비는 .dp를 사용하여 dp로 변환해야 합니다. TextMeasurer는 픽셀 단위로 크기를 반환하지만 Modifier.width는 Dp를 기대하기 때문입니다. 정확성을 위해 density와 LocalDensity.current를 사용하세요.
여러 줄 텍스트의 경우 TextMeasurer는 전체 높이뿐만 아니라 줄 수와 단락 내 각 문자의 위치도 결정할 수 있습니다. 이는 커스텀 텍스트 필드, 채팅 또는 편집기를 만들 때 필요합니다.
여러 줄 텍스트의 주요 TextLayoutResult 속성: lineCount(줄 수), getLineTop(index)(줄 시작의 Y 좌표), getLineBottom(index)(줄 끝의 Y 좌표), getLineWidth(index)(줄 너비). 이들을 결합하여 텍스트 줄을 기준으로 인터페이스 요소를 정확하게 배치할 수 있습니다.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Long multiline text that needs to be measured before layout",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 150, maxHeight = 300)
)
val linesCount = result.lineCount
val firstLineWidth = result.getLineWidth(0)
val totalHeight = result.size.height
getLineWidth(lineIndex) 메서드는 지정된 줄의 너비를 픽셀 단위로 반환합니다. 줄이 컨테이너 경계에 도달하지 않으면 maxWidth보다 작을 수 있습니다. 각 줄의 너비를 알면 수동으로 양쪽 정렬 효과를 구현하거나 짧은 줄 옆에 인라인 요소를 배치할 수 있습니다.
특정 문자의 위치를 얻으려면 getBoundingBox(offset)을 사용하세요. 이는 left, top, right, bottom 좌표가 있는 Rect를 반환합니다. 이는 커스텀 텍스트 필드에서 커서 위치를 지정하거나 애니메이션으로 텍스트 범위를 강조 표시하는 데 유용합니다.
TextMeasurer의 가장 강력한 응용 분야는 커스텀 Layout 컴포넌트로, 텍스트가 다른 요소와 공존하고 크기가 상호 의존적인 경우입니다. 예를 들어, 아이콘이 전체 블록이 아닌 텍스트의 첫 번째 줄에 상대적으로 중앙에 배치되어야 하는 “아이콘 + 텍스트” 위젯입니다.
표준 Row에서 아이콘은 전체 Row 높이로 중앙에 배치되지만, 텍스트가 3줄이고 아이콘이 한 줄일 때는 부자연스럽게 보입니다. TextMeasurer를 사용하면 첫 번째 줄을 측정하고 아이콘을 해당 줄에 정확히 정렬할 수 있습니다.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Multi-line text here")
},
measurePolicy = { measurables, constraints ->
val textMeasurable = measurables[1]
val textPlaceable = textMeasurable.measure(constraints)
val iconPlaceable = measurables[0].measure(constraints)
val firstLineHeight = textMeasurer.measure(
text = "Sample",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 2000, maxHeight = 100)
).size.height
layout(width = textPlaceable.width + iconPlaceable.width,
height = maxOf(textPlaceable.height, iconPlaceable.height)) {
iconPlaceable.placeRelative(0, (firstLineHeight - iconPlaceable.height) / 2)
textPlaceable.placeRelative(iconPlaceable.width, 0)
}
}
)
이 예제에서 TextMeasurer를 통한 첫 번째 줄 측정이 아이콘 위치를 결정합니다. TextMeasurer가 없으면 아이콘은 여러 줄 텍스트의 전체 높이로 중앙에 배치되어 시각적으로 부정확합니다. 이 패턴은 채팅, 연락처 목록 및 정보 카드에서 널리 사용됩니다.
TextMeasurer는 UI 스레드에서 실행되는 동기 API입니다. 과도하게 사용하면 특히 약한 기기에서 큰 텍스트 볼륨을 측정할 때 프레임 드롭이 발생할 수 있습니다.
첫 번째 규칙 — 결과를 캐시하세요. 항상 measure() 호출을 텍스트 및 스타일 종속성과 함께 remember로 감싸세요. CompositionLocal 내부나 핫 재구성 경로에서 measure()를 호출하지 마세요.
두 번째 규칙 — maxWidth와 maxHeight를 제한하세요. maxWidth = Constraints.Infinity로 설정하면 TextMeasurer가 줄을 줄바꿈할 수 없어 텍스트가 화면 밖으로 나갑니다. 특히 여러 줄 텍스트의 경우 항상 현실적인 제약 조건을 전달하세요.
세 번째 규칙 — 동일한 매개변수로 반복 측정을 피하세요. 텍스트와 스타일이 변경되지 않은 경우 measure() 결과는 동일합니다. 비동기 시나리오의 경우 측정을 Deferred 또는 LaunchedEffect와 결합하세요.
자주 묻는 질문
TextMeasurer는 렌더링 전에 텍스트를 측정하고, onSizeChanged는 이후에 측정합니다. TextMeasurer는 텍스트 크기가 부모 레이아웃에 영향을 미칠 때 필요합니다. onSizeChanged는 구성 후 반응형 로직에 적합합니다.
네, TextMeasurer.measure()는 인라인 스타일, 링크 및 색상이 포함된 SpannableString을 포함하는 AnnotatedString을 허용합니다. 모든 스팬은 측정 시 고려됩니다.
TextMeasurer는 모든 크기가 픽셀 단위인 Skia 렌더링 수준에서 작동합니다. LocalDensity.current를 통해 변환하세요: density.run { width.toDp() }.
TextMeasurer는 커스텀 글꼴이 로드될 때까지 FontFamily.Default를 사용합니다. 정확한 측정을 위해 FontResource를 통해 글꼴 로드를 기다리고 async/await를 사용하세요.
각 LazyColumn 항목에서 텍스트를 측정하면 약한 기기에서 FPS가 감소할 수 있습니다. 최적화: remember를 통해 결과를 캐시하고 텍스트 크기를 제한하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.