onMeasure()는 android.view.View 클래스의 protected 메서드로, Android 시스템이 View의 크기를 결정하기 위해 호출합니다. 시스템은 메서드에 두 개의 MeasureSpec 객체를 전달하며, 각각 측정 모드(EXACTLY, AT_MOST 또는 UNSPECIFIED)와 부모 컨테이너가 제안하는 크기를 포함합니다. Android Developers Documentation(2026)에 따르면, 정확한 크기 제어가 필요한 모든 커스텀 View 및 ViewGroup에서 MeasureSpec을 올바르게 처리하는 onMeasure 재정의는 필수 단계입니다.
핵심 사항
onMeasure(int widthMeasureSpec, int heightMeasureSpec)는 View 클래스의 메서드로, Android 시스템이 뷰의 너비와 높이를 결정하기 위해 호출합니다. 개발자는 이 메서드를 재정의하여 MeasureSpec에 전달된 제약 조건을 기반으로 View의 크기를 지정합니다. 올바른 onMeasure 재정의 없이는 커스텀 View가 잘못 표시되거나 전혀 나타나지 않을 수 있습니다.
시스템은 View 라이프사이클의 measure 단계에서 onMeasure를 호출하며, 이는 layout(onLayout) 및 draw(onDraw) 단계보다 먼저 발생합니다. View가 onMeasure를 재정의하지 않으면 슈퍼클래스의 구현이 사용되며, background drawable 또는 layout_params를 기반으로 기본 크기를 설정합니다. super.onMeasure(widthMeasureSpec, heightMeasureSpec) 호출은 TextView 또는 ImageView와 같은 표준 View 서브클래스에서만 작동합니다.
onMeasure의 핵심 요구 사항은 setMeasuredDimension(int, int) 호출이 메서드 끝에 반드시 존재해야 한다는 것입니다. 이 호출이 없으면 시스템은 View가 측정 치수를 설정하지 않았다는 IllegalStateException을 발생시킵니다. 측정 단계가 완료된 후 최종 치수는 getMeasuredWidth() 및 getMeasuredHeight() 게터를 통해 사용할 수 있습니다.
MeasureSpec은 32비트 정수로, 상위 2비트는 측정 모드를 인코딩하고 하위 30비트는 크기를 인코딩합니다. 모드는 View가 자체 크기를 선택할 수 있는 자유도를 결정합니다. Android는 EXACTLY, AT_MOST, UNSPECIFIED의 세 가지 모드를 제공합니다. 각 모드는 onMeasure에서 다른 처리 로직을 지시합니다.
| MeasureSpec 모드 | 값 | 동작 |
|---|---|---|
| EXACTLY | 부모가 정확한 크기를 지정함 | View는 경계를 벗어나지 않으려면 주어진 크기에 정확히 맞아야 함 |
| AT_MOST | 부모가 최대 크기를 설정함 | View는 0에서 주어진 최대값까지任意의 크기를 선택할 수 있음 |
| UNSPECIFIED | 부모가 제약을 가하지 않음 | View는 상한선 없이 원하는 크기를 선택할 수 있음 |
MeasureSpec에서 모드와 크기를 추출하려면 MeasureSpec 클래스의 정적 메서드를 사용합니다: MeasureSpec.getMode(int)는 세 가지 모드(EXACTLY, AT_MOST, UNSPECIFIED) 중 하나를 반환하고, MeasureSpec.getSize(int)는 픽셀 단위의 숫자 크기를 반환합니다. 커스텀 MeasureSpec을 생성하려면 MeasureSpec.makeMeasureSpec(int size, int mode)를 사용합니다. 이 세 가지 메서드는 onMeasure에서 크기 작업의 모든 시나리오를 다룹니다.
표준 패턴: 모드가 EXACTLY이면 전달된 크기를 최종 크기로 사용합니다. AT_MOST이면 원하는 크기(View의 콘텐츠)와 전달된 최대값 중 최소값을 선택합니다. UNSPECIFIED이면 제약 없이 View의 원하는 크기를 사용합니다. 이 패턴은 부모의 모든 제약 조건에서 올바른 동작을 보장합니다.
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val desiredWidth = 200
val desiredHeight = 100
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
표준 로직을 단순화하기 위해 Android는 resolveSizeAndState 메서드를 제공합니다. 이 메서드는 원하는 크기와 MeasureSpec을 받아 올바른 모드로 최종 크기를 반환합니다. 이 메서드는 위에서 설명한 패턴을 한 줄의 코드로 구현합니다. resolveSize(int size, int measureSpec) 함수도 사용할 수 있으며, 상태 비트 없이 깔끔한 크기를 반환합니다.
Android는 2패스 측정 알고리즘을 사용하여 계층 구조의 모든 View가 부모 제약 조건과 자식 선호도를 고려하여 올바른 치수를 얻도록 보장합니다. 첫 번째 패스에서 부모는 제약 조건이 있는 MeasureSpec을 자식 View에 전달하고, 자식 View는 원하는 크기를 계산합니다. 두 번째 패스에서 부모는 크기에 대한 최종 결정을 내립니다.
ViewGroup의 경우 측정 프로세스가 더 복잡합니다. 부모는 먼저 모든 자식을 측정한 다음, 자식의 크기를 기반으로 자체 크기를 결정해야 합니다. measureChildren(int widthMeasureSpec, int heightMeasureSpec)을 호출하면 모든 자식 View를 반복하고 각각에 대해 measure(child, childWidthSpec, childHeightSpec)을 호출합니다. 모든 자식을 측정한 후 ViewGroup은 자체 치수로 setMeasuredDimension을 호출합니다.
중요한 세부 사항: measure 메서드(public, final)는 재정의할 수 없습니다. 대신 onMeasure가 재정의됩니다. 이를 통해 시스템은 크기 변경 확인 및 후속 그리기를 위한 더티 영역 계산과 같은 유지 관리 작업을 onMeasure 전후에 수행할 수 있습니다. View에 고정 치수가 있는 경우 onMeasure 재정의가 필요하지 않을 수 있습니다.
MeasureSpec에는 크기와 모드뿐만 아니라 MeasureSpec.getMode()를 통해 액세스할 수 있는 상태 비트도 포함됩니다. setMeasuredDimension을 호출한 후 상태는 View의 측정 치수의 일부가 되며 getMeasuredState()를 통해 확인할 수 있습니다. 이는 ScrollView 및 기타 스크롤 가능한 컨테이너에서 자식에게 제약 조건을 올바르게 전달하는 데 사용됩니다.
정사각형 표시를 위해 onMeasure를 재정의한 커스텀 View를 만드는 실제 예제를 살펴보겠습니다. SquareView 클래스는 View를 확장하며 전달된 MeasureSpec에 관계없이 너비와 높이가 항상 동일하도록 보장합니다. onMeasure에서는 최소 변을 결정하고 정사각형 크기를 설정합니다.
class SquareView(context: Context)
: View(context) {
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val widthSize =
MeasureSpec.getSize(widthMeasureSpec)
val heightSize =
MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(widthSize, heightSize)
setMeasuredDimension(size, size)
}
}
ViewGroup은 더 복잡한 onMeasure 로직이 필요합니다. 먼저 자식을 측정한 다음 ViewGroup 자체의 크기를 결정해야 하기 때문입니다. CascadeLayout 예제는 자식을 오프셋이 있는 캐스케이드 형태로 배포합니다. measureChildWithMargins를 통해 모든 자식을 측정한 후 전체 너비와 높이가 계산됩니다.
class CascadeLayout(context: Context)
: ViewGroup(context) {
private val cascadeOffset = 40
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
var maxWidth = 0
var totalHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child,
widthMeasureSpec,
cascadeOffset * i,
heightMeasureSpec, 0)
maxWidth = maxOf(maxWidth,
child.measuredWidth +
cascadeOffset * i)
totalHeight += child.measuredHeight
}
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec))
}
override fun generateLayoutParams(attrs: AttributeSet?)
: LayoutParams = MarginLayoutParams(context, attrs)
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val left = l + cascadeOffset * i
child.layout(left, top,
left + child.measuredWidth,
top + child.measuredHeight)
top += child.measuredHeight
}
}
}
setMeasuredDimension 호출 누락은 가장 흔한 실수입니다. 개발자가 onMeasure를 재정의했지만 setMeasuredDimension을 호출하지 않으면 애플리케이션이 IllegalStateException과 함께 충돌합니다. 이는 특히 메서드에 조건부 분기가 있고 한 분기에 호출이 없는 경우 자주 발생합니다. onMeasure의 모든 코드 분기는 setMeasuredDimension 호출로 끝나야 합니다.
AT_MOST 모드 무시는 두 번째로 흔한 실수입니다. AT_MOST 모드의 View가 콘텐츠를 기반으로 계산하는 대신 항상 전달된 크기를 사용하면 부모 컨테이너가 공간을 올바르게 분배할 수 없습니다. 예를 들어 AT_MOST의 TextView는 텍스트 너비를 계산하고 원하는 너비와 전달된 너비 중 최소값을 사용해야 합니다. AT_MOST를 무시하면 View가 작은 콘텐츠에서도 모든 사용 가능한 공간을 차지합니다.
onMeasure 내에서 객체 생성은 고전적인 성능 실수입니다. onMeasure는 여러 번 호출될 수 있으므로(모든 레이아웃 요청 시) 이 메서드 내에서 객체(Paint, Rect, String)를 생성하면 메모리를 오염시키고 가비지 컬렉션을 유발합니다. 모든 객체는 View 생성자에서 한 번 생성되어야 하며, onMeasure에서는 크기 계산 로직만 실행되어야 합니다. 동일한 규칙이 onDraw 및 onLayout에도 적용됩니다.
measureChildWithMargins는 ViewGroup의 protected 메서드로, MarginLayoutParams를 고려하여 단일 자식 View를 측정합니다. 이 메서드는 부모의 MeasureSpec과 누적된 너비 및 높이 오프셋을 받습니다. 부모 패딩과 자식 마진을 빼서 자식 View의 MeasureSpec을 자동으로 조정한 후 조정된 MeasureSpec을 child.measure()에 전달합니다.
고급 측정 로직의 경우 ViewGroup은 measureChild(View child, int parentWidthSpec, int parentHeightSpec)을 재정의하거나 각 자식에 대해 직접 MeasureSpec으로 작업할 수 있습니다. 예를 들어 LinearLayout은 onMeasure에서 모든 자식 View를 반복하고, 각각의 layout_weight를 고려하여 측정한 후 나머지 공간을 비례적으로 분배합니다. 이 접근 방식을 통해 임의의 레이아웃 알고리즘을 구현할 수 있습니다.
측정 캐시 메커니즘을 통한 측정 결과 캐싱은 특정 ViewGroup에서 setMeasureWithLargestChildEnabled 플래그를 통해 사용할 수 있습니다. 그러나 대부분의 경우 onMeasure는 레이아웃 변경 시마다 다시 호출되며 캐싱이 적용되지 않습니다. 커스텀 ViewGroup에서는 캐싱에 의존하기보다 onMeasure에서 계산을 최소화하는 것이 좋습니다.
자주 묻는 질문
네, 커스텀 View가 View 클래스에서 직접 상속받는 경우 필요합니다. TextView, ImageView 또는 Button에서 표준 크기로 상속받는 경우 onMeasure를 변경하지 않고 그대로 둘 수 있습니다. ViewGroup의 경우 항상 onMeasure 재정의가 필요합니다. 그렇지 않으면 자식이 올바르게 측정되지 않습니다.
Android 시스템은 “The View did not call setMeasuredDimension” 메시지와 함께 IllegalStateException을 발생시킵니다. 이 예외는 onMeasure 완료 후 measure() 메서드에서 최종 치수가 0으로 남아 있을 때 발생합니다. try-catch로 처리하지 않으면 애플리케이션이 충돌합니다.
getMeasuredWidth()는 onMeasure(측정 단계)에서 설정된 크기를 반환합니다. getWidth()는 onLayout에서 위치 조정 후 View가 받은 실제 크기를 반환합니다. 대부분의 View에서 이 값들은 일치하지만, 커스텀 ViewGroup에서는 다를 수 있습니다.
아니요, onMeasure는 크기 계산 전용입니다. 이 메서드 내에서 상태 변경, 애니메이션 시작, 네트워킹 또는 데이터 업데이트를 수행하면 Android 아키텍처를 위반하고 재귀적인 measure 호출을 유발할 수 있습니다. 상태 변경이 requestLayout을 트리거할 수 있기 때문입니다.
ConstraintLayout은 정의된 제약 조건을 기반으로 자식 측정을 독립적으로 관리합니다. ConstraintLayout 내의 커스텀 View가 onMeasure를 재정의하는 경우 ConstraintLayout에서 전달된 MeasureSpec을 올바르게 처리해야 합니다. 그렇지 않으면 제약 조건이 작동하지 않을 수 있습니다. ConstraintLayout은 계산을 위해 자체 WidgetContainer를 사용하는 2패스 알고리즘을 사용합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.