View Lifecycle — 무엇인가, onMeasure onLayout onDraw 프로세스

저자: IT Sectr 게시일: 2026-03-05 읽는 시간: 10 분

View Lifecycle — Android가 화면에서 사용자 인터페이스 요소(View)를 그리고 다시 그리기 위해 호출하는 메서드의 시퀀스입니다. Activity나 Fragment와 달리 View는 확장된 생명주기가 없는 경량 컴포넌트이지만, 엄격한 3단계 프로세스(onMeasure(측정), onLayout(배치), onDraw(그리기))를 거칩니다. View Lifecycle을 이해하는 것은 커스텀 View 생성, 성능 최적화 및 그리기 문제 해결에 필요합니다. Google에 따르면, 커스텀 View는 올바르게 구현될 때 표준 중첩 ViewGroup 조합과 비교하여 UI를 15–40% 향상시킵니다. 커스텀 View에 대한 Android 문서는 onMeasure, onLayout, onDraw를 View Lifecycle의 세 가지 기둥으로 설명합니다.

핵심 내용

  • View Lifecycle은 세 단계로 구성됩니다: onMeasure(크기), onLayout(위치), onDraw(그리기) — invalidate() 또는 requestLayout()에 의해 트리거됩니다.
  • onMeasure는 MeasureSpec(AT_MOST, EXACTLY, UNSPECIFIED)을 기반으로 View의 너비와 높이를 계산합니다.
  • onLayout은 ViewGroup 내에서 자식 View를 배치하고 left, top, right, bottom 좌표를 결정합니다.
  • onDraw는 View 콘텐츠를 Canvas에 렌더링합니다: 배경, 텍스트, 도형, 이미지.
  • 잘못된 View Lifecycle은 UI 성능 문제(jank, 드롭된 프레임) 및 계층 구조 문제의 주요 원인입니다.

View Lifecycle — Android에서 무엇인가

View Lifecycle은 Android View(및 ViewGroup)가 화면에 자신을 표시하기 위해 거치는 프로세스입니다. Activity나 Fragment와 달리 View에는 onStart/onStop/onDestroy가 없습니다 — 그 “수명”은 측정, 배치 및 그리기의 순환 프로세스로 구성됩니다. 이 사이클은 View를 표시하거나 다시 그려야 할 때마다 트리거됩니다.

View Lifecycle의 세 단계:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — View의 원하는 치수를 결정합니다. 시스템이 MeasureSpec을 전달합니다 — 허용되는 치수(정확한 값, 최대값 또는 제한 없음)에 대한 지침입니다.
  • onLayout(boolean changed, int left, int top, int right, int bottom) — View와 그 자식을 화면에 배치합니다. View의 경우 자체 경계를 정의하고 ViewGroup의 경우 자식 요소의 위치를 정의합니다.
  • onDraw(Canvas canvas) — 제공된 Canvas에 View 콘텐츠를 그립니다. 시스템은 명령을 비트맵 또는 GPU로 변환하는 Canvas를 제공합니다.

전체 View Lifecycle 사이클에는 View를 창에 연결하는 관련 메서드도 포함됩니다: onAttachedToWindow(View가 창에 연결됨, HW 가속 있음) 및 onDetachedFromWindow(View가 분리됨, 리소스 해제됨). 이러한 메서드는 View 수명 동안 한 번 호출되며 애니메이션/센서 등록 및 취소에 중요합니다.

Android Performance Blog에 따르면, UI 성능 문제(jank, 프레임 드롭)의 65%는 onMeasure 및 onDraw의 잘못된 구현과 관련됩니다: 과도한 재정의, 불필요한 requestLayout() 호출, onDraw에서 객체 생성.

onMeasure: View 치수 측정

onMeasure — View Lifecycle에서 가장 중요하고 가장 복잡한 단계. 이 단계에서 Android는 View가 화면에서 얼마나 많은 공간을 차지할지 결정합니다. 시스템이 MeasureSpec을 전달합니다 — 모드와 크기로 구성된 int로 압축된 명령입니다.

세 가지 MeasureSpec 모드:

모드상수의미
EXACTLYMeasureSpec.EXACTLY부모가 설정한 정확한 크기(match_parent 또는 고정 너비)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView는 지정된 최대 크기까지 가능(wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIED제한 없음 — View는 모든 크기 가능(ScrollView, RecyclerView)너비 무제한 → MeasureSpec(0, UNSPECIFIED)

onMeasure 구현은 다음을 수행해야 합니다:

  • 측정된 치수를 저장하기 위해 setMeasuredDimension(int width, int height)를 호출합니다.
  • 패딩을 고려합니다 — 사용 가능한 너비에서 getPaddingLeft() + getPaddingRight()를 뺍니다.
  • ViewGroup의 경우 — measureChild() 또는 measureChildWithMargins()를 통해 모든 자식을 측정합니다.
  • wrap_content의 경우 — 콘텐츠(텍스트, 이미지)를 기반으로 크기를 계산합니다.
  • onMeasure 내에서 requestLayout()을 호출하지 않습니다 — 무한 루프가 발생합니다.

일반적인 실수: wrap_content 사용 시 MeasureSpec을 고려하지 않는 것. View가 wrap_content로 설정되었지만 onMeasure가 AT_MOST를 처리하지 않고 고정 크기를 반환하면 View가 잘리거나 필요한 것보다 더 많은 공간을 차지합니다.

onLayout: 화면에 View 배치

onLayout — View 또는 ViewGroup이 자식을 자신의 경계 내에 정렬하는 단계. 일반 View(ViewGroup 아님)의 경우 onLayout이 필요하지 않습니다 — 시스템이 부모로부터 전달된 매개변수로 layout()을 호출합니다. ViewGroup의 경우 onLayout이 필수입니다 — 없으면 자식 View가 배치되지 않습니다.

onLayout 시그니처:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // 자식 View 배치
}

changed 매개변수는 이전 레이아웃과 비교하여 View의 위치나 크기가 변경되었는지 여부를 나타냅니다. false인 경우 View는 최적화를 위해 자식 위치 재계산을 건너뛸 수 있습니다.

ViewGroup의 경우 onLayout은 다음을 수행해야 합니다:

  • getChildCount()getChildAt(i)를 통해 모든 자식을 반복합니다.
  • 각 자식에 대해 left, top, right, bottom을 결정합니다 — ViewGroup 내 좌표(패딩 고려).
  • 각 자식에 대해 child.layout(l, t, r, b)를 호출합니다.
  • 그래비티, 마진, 정렬을 고려합니다.

onMeasure 후에 onLayout이 호출됩니다 — 측정된 치수는 getMeasuredWidth()/getMeasuredHeight()를 통해 사용할 수 있습니다. layout() 후 자식 View의 실제 치수가 다른 경우 requestLayout()이 재측정을 위해 호출됩니다. 이것을 “layout 패스”라고 하며 재계산의 연쇄 반응을 일으킬 수 있습니다.

onDraw: Canvas 콘텐츠 그리기

onDraw — View가 Canvas에 자신을 그리는 단계. 이것은 onMeasure 및 onLayout 없이 여러 번 호출될 수 있는 유일한 단계입니다 — View가 invalidate()로 표시된 경우. Canvas는 그리기 API를 제공합니다: drawLine, drawRect, drawCircle, drawText, drawBitmap 및 drawPath.

onDraw 규칙:

  • onDraw에서 객체를 생성하지 마십시오 — 각 onDraw 호출은 미리 생성된 객체(Path, Paint, Rect)를 사용해야 합니다. onDraw에서 객체를 생성하면 GC 일시 중지 및 드롭된 프레임이 발생합니다.
  • onDraw 내에서 requestLayout() 또는 invalidate()를 호출하지 마십시오 — 무한 다시 그리기 루프가 발생합니다.
  • 긴 계산을 수행하지 마십시오 — onDraw는 UI 스레드에서 실행됩니다. 복잡한 계산은 백그라운드 스레드로 이동하거나 미리 계산해야 합니다.
  • 하드웨어 가속을 사용하십시오 — API 14+부터 Canvas는 GPU를 통해 작동할 수 있습니다. 복잡한 그래픽(그라데이션, 그림자, 회전)의 경우 HW 가속은 최대 300%의 성능 향상을 제공합니다.
  • 보이는 영역만 그리십시오 — 보이지 않는 부분을 자르려면 canvas.clipRect()를 사용하십시오.

ViewGroup의 그리기 순서: 배경(setBackgroundDrawable) → onDraw(콘텐츠) → dispatchDraw(자식 View) → onDrawForeground(전경). dispatchDraw는 각 자식의 onDraw를 호출합니다. dispatchDraw 재정의는 자식 요소 위에 효과를 적용하는 데 사용됩니다.

Android Vitals 통계에 따르면, onDraw에서 프레임 드롭의 가장 일반적인 원인은 메서드 내 객체 생성(48%), decodeResource 호출(22%), 캐싱 없는 복잡한 Path 작업(15%)입니다.

무효화: View가 다시 그려지는 시점

무효화 — View 다시 그리기를 트리거하는 메커니즘. invalidate()를 호출하면 View가 “더티”로 표시되고 다음 그리기 사이클에서 onDraw 호출이 예약됩니다. requestLayout() 호출은 더 “무거운” 작업으로 전체 사이클을 트리거합니다: onMeasure → onLayout → onDraw.

메서드기능사용 시점
invalidate()onMeasure/onLayout 없이 onDraw 트리거외관만 변경(색상, 텍스트, 진행률)
invalidate(Rect)지정된 영역만 다시 그리기View의 일부 변경 — 애니메이션, 선택
postInvalidate()비UI 스레드에서 invalidate 호출백그라운드 스레드가 그리기 데이터 업데이트
requestLayout()onMeasure → onLayout → onDraw 트리거콘텐츠 크기 변경(텍스트, 이미지)
forceLayout()강제 재측정을 위해 View 표시내부 상태 변경, 크기가 변경될 수 있음

애니메이션 및 View Lifecycle: ViewPropertyAnimator와 ValueAnimator는 각 애니메이션 프레임에서 invalidate()를 호출합니다. ObjectAnimator는 View에서 setter를 호출하며, setter가 크기(너비/높이)를 변경하면 자동으로 requestLayout()을 호출합니다. 이는 복잡한 ViewGroup에서 비용이 많이 들 수 있습니다: 각 requestLayout이 루트 View까지 전체 계층 구조를 트리거합니다.

최적화 규칙: 외관만 변경되는 곳(색상, 투명도, 크기 변경 없는 회전)에서는 requestLayout() 대신 invalidate()를 사용하십시오. requestLayout은 크기나 크기에 영향을 주는 콘텐츠를 변경할 때만 사용하십시오.

커스텀 View 최적화: 모범 사례

커스텀 View는 고유한 UI를 만들기 위한 강력한 도구이지만 성능 규칙을 엄격히 준수해야 합니다. View Lifecycle 최적화를 위한 Google의 주요 권장 사항은 다음과 같습니다.

  • 계산할 수 있는 것은 모두 미리 계산하십시오 — 크기, 좌표, 경로, 그라데이션 색상. onDraw에서는 그리기만 수행하십시오.
  • 측정 결과를 캐싱하십시오 — View에 고정 치수가 있는 경우 MeasureSpec을 저장하고 추가 계산 없이 setMeasuredDimension을 반환하십시오.
  • ViewConfiguration을 사용하십시오 — getScaledTouchSlop, getScaledMinimumFlingVelocity — 터치 처리용.
  • 계층 구조에서 View 수를 최소화하십시오 — 여러 요소를 결합하는 커스텀 View는 항상 3–5개의 중첩 View가 있는 ViewGroup보다 빠릅니다. Google은 화면당 10개 이하의 중첩 View를 권장합니다.
  • 플랫 계층 구조에는 ConstraintLayout을 사용하십시오 — RelativeLayout에 가까운 성능으로 단일 ViewGroup을 구축하지만 중첩이 없습니다.
  • 다시 그리기 후 하드웨어 레이어를 비활성화하십시오 — 애니메이션이 있는 View에는 setLayerType(LAYER_TYPE_HARDWARE)을, 완료 후에는 setLayerType(LAYER_TYPE_NONE)을 사용하십시오.
  • Rect와 함께 invalidate()를 사용하십시오 — 전체 View가 아닌 변경된 영역만 다시 그리십시오.
  • 오버드로우를 피하십시오 — 불필요한 다시 그리기를 식별하려면 Android Studio에서 Profile GPU Rendering을 사용하십시오. Google 앱의 평균 오버드로우는 1.5배, 권장 최대치는 2.5배입니다.

Kotlin의 View 코드 예제

예제 1: 커스텀 View — 진행률 표시기

onMeasure, onDraw 및 invalidate의 올바른 구현이 있는 간단한 원형 진행률 표시기입니다.

kotlin
class CircularProgressView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
    }

    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    private var progress = 0f
    private var viewWidth = 0
    private var viewHeight = 0

    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val desiredSize = 100 * resources.displayMetrics.density.toInt()
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(width, height).coerceAtLeast(desiredSize)
        setMeasuredDimension(size, size)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val padding = progressPaint.strokeWidth / 2
        val radius = (minOf(viewWidth, viewHeight) - padding) / 2
        val cx = viewWidth / 2f
        val cy = viewHeight / 2f
        canvas.drawCircle(cx, cy, radius, backgroundPaint)
        val sweepAngle = (progress / 100f) * 360f
        canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
            -90f, sweepAngle, false, progressPaint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
    }
}

원형 진행률 표시줄: onMeasure는 MeasureSpec을 기반으로 정사각형 크기를 반환하고, onSizeChanged는 치수를 기억하며, onDraw는 배경과 진행률 호를 그립니다. 진행률이 변경되면 Invalidate가 호출됩니다 — onMeasure/onLayout은 영향을 받지 않습니다. Paint는 생성자에서 한 번 생성되며 onDraw에서 생성되지 않습니다.

예제 2: ViewGroup — 간단한 FlowLayout

자식 View를 행으로 배치하는 커스텀 ViewGroup입니다(Flexbox wrap처럼).

kotlin
class FlowLayout constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private val horizontalSpacing = 8.dpToPx(resources)
    private val verticalSpacing = 8.dpToPx(resources)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        var totalHeight = paddingTop + paddingBottom
        var rowWidth = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
            if (rowWidth + child.measuredWidth > width - paddingRight) {
                totalHeight += rowHeight + verticalSpacing
                rowWidth = paddingLeft
                rowHeight = 0
            }
            rowWidth += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
        totalHeight += rowHeight
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean,
        l: Int, t: Int, r: Int, b: Int) {
        var rowTop = paddingTop
        var rowLeft = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (rowLeft + child.measuredWidth > r - paddingRight) {
                rowTop += rowHeight + verticalSpacing
                rowLeft = paddingLeft
                rowHeight = 0
            }
            child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
            rowLeft += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

FlowLayout은 onMeasure를 재정의합니다: 각 자식을 측정하고, 너비를 초과하면 새 행으로 줄 바꿈하고, 전체 높이를 계산합니다. onLayout은 줄 바꿈을 고려하여 좌표로 자식을 배치합니다. generateLayoutParams는 자식 View의 마진을 지원하기 위해 MarginLayoutParams를 반환합니다.

예제 3: Path 캐싱을 사용한 onDraw

부드러운 베지에 곡선을 그리고 Path를 미리 계산하여 캐싱하는 커스텀 View입니다.

kotlin
class WaveView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#4A90D9")
        style = Paint.Style.FILL
    }

    private val wavePath = Path()
    private var isPathDirty = true
    private var viewWidth = 0
    private var viewHeight = 0

    fun refreshWave() {
        isPathDirty = true
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        isPathDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (isPathDirty) {
            wavePath.reset()
            val amplitude = viewHeight * 0.1f
            wavePath.moveTo(0f, viewHeight * 0.5f)
            for (x in 0..viewWidth step 4) {
                val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
                wavePath.lineTo(x.toFloat(), y)
            }
            wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
            wavePath.lineTo(0f, viewHeight.toFloat())
            wavePath.close()
            isPathDirty = false
        }
        canvas.drawPath(wavePath, wavePaint)
    }
}

Path 캐싱: isPathDirty = true는 View 치수가 변경되거나 refreshWave()가 호출된 경우에만 true입니다. onDraw에서 Path는 “더티”인 경우에만 다시 계산됩니다. 이는 모든 애니메이션 프레임에서 베지에 곡선 재계산을 방지하여 CPU를 절약합니다.

자주 묻는 질문

View Lifecycle과 Activity Lifecycle의 차이점은 무엇인가요?

View Lifecycle은 Activity 생성/소멸과 독립적인 순환 그리기 프로세스(onMeasure → onLayout → onDraw)입니다. View에는 onStart/onStop이 없습니다 — 표시 가능(창에 연결)하거나 그렇지 않습니다. Activity Lifecycle은 애플리케이션 컴포넌트 상태를 관리하고 View Lifecycle은 UI 그리기를 관리합니다.

requestLayout()이 성능에 미치는 영향은 무엇인가요?

requestLayout()은 루트에서 전체 View 트리에 대한 완전한 onMeasure → onLayout → onDraw 사이클을 트리거합니다. requestLayout()을 자주 호출하면(예: 모든 애니메이션 프레임) jank 및 드롭된 프레임이 발생합니다. Google에 따르면 10개 요소의 ViewGroup에서 한 번의 requestLayout은 평균 2–5ms가 소요됩니다. 애니메이션의 경우 invalidate()를 사용하십시오.

onAttachedToWindow는 언제 호출되나요?

onAttachedToWindow는 View가 Window에 연결될 때 호출됩니다 — 표시 가능한 계층 구조의 일부가 됩니다. 이 시점에서 View는 HW 가속 및 Window 리소스(WindowManager, Display)에 대한 액세스를 받습니다. onAttachedToWindow는 View가 표시되는 동안 유지되는 애니메이션 리스너 및 BroadcastReceiver를 등록하기에 적합한 장소입니다.

오버드로우란 무엇이며 어떻게 줄이나요?

오버드로우는 단일 프레임에서 픽셀이 여러 번 그려지는 상황입니다. 추가 패스마다 GPU 시간이 낭비됩니다. 줄이는 방법: 테마에 windowBackground 설정(layout에서 배경 그리지 않음), canvas.clipRect() 사용, 중첩 배경 병합 방지, 중첩 LinearLayout 대신 ConstraintLayout 사용. Android Studio → Profile GPU Rendering → Overdraw는 오버드로우의 색상 맵을 표시합니다(파랑=1배, 빨강=3배+).

커스텀 View에서 super.onDraw()가 필요한가요?

네, View에 배경이 있는 경우. super.onDraw()는 View의 배경을 그립니다. 커스텀 View에 배경이 없거나 자체 배경을 그리는 경우 super.onDraw()를 생략할 수 있습니다 — 이로 인해 한 번의 그리기 패스가 절약됩니다. ViewGroup의 경우 super.dispatchDraw()가 필수입니다 — 자식 View를 그립니다.

요약

  • View Lifecycle — 세 가지 그리기 단계: onMeasure(치수), onLayout(위치), onDraw(렌더링).
  • onMeasure는 MeasureSpec(EXACTLY, AT_MOST, UNSPECIFIED)을 처리하고 setMeasuredDimension을 호출합니다.
  • ViewGroup의 onLayout은 left/top/right/bottom 좌표로 자식 View를 배치합니다.
  • onDraw는 Canvas에 콘텐츠를 렌더링합니다 — 이 메서드 내에서 객체를 생성하지 마십시오.
  • invalidate()는 onDraw만 트리거하고 requestLayout()은 전체 onMeasure → onLayout → onDraw 사이클을 트리거합니다.
  • 커스텀 View는 UI를 15–40% 향상시키지만 올바른 onMeasure 구현과 onDraw에서의 객체 캐싱이 필요합니다.
  • 복잡한 그래픽의 경우 하드웨어 가속을 사용하고 Path/Bitmap을 캐싱하십시오.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기