onDraw() — 개념, 호출 메커니즘 및 메소드 오버라이드

저자: IT Sectr 게시일: 2026-07-22 읽는 시간: 10 분

onDraw()는 Android SDK에서 View 클래스의 핵심 메소드로, 뷰의 콘텐츠를 그리는 역할을 담당합니다. 시스템은 View의 외관이 업데이트될 때마다 onDraw를 호출합니다 — 초기 렌더링 중, invalidate() 이후, 또는 차원이 변경될 때입니다. Android Developers Documentation (2026)에 따르면, Canvas API를 사용한 onDraw 오버라이드는 Android 애플리케이션에서 커스텀 그래픽을 만드는 기본 방법입니다.

주요 결과

  • onDraw(Canvas) — Canvas에 콘텐츠를 그리기 위해 시스템이 호출하는 View 메소드
  • invalidate() — View 재그림 요청, 다음 프레임 사이클에서 onDraw 호출 유발
  • Canvas API는 drawCircle, drawBitmap, drawText, drawPath 등의 그림 메소드를 제공함
  • Android 시스템은 UI 스레드에서 그림을 수행하므로, onDraw는 가벼우고 빠르여야 함
  • Hardware acceleration은 Android 3.0 (API 11)부터 GPU를 통해 그림을 자동으로 가속함

onDraw()란?

onDraw(Canvas canvas)는 android.view.View 클래스의 protected 메소드로, View를 그리지 않으면 안 될 때 Android 시스템이 호출합니다. 개발자는 커스텀 그림(도형, 텍스트, 이미지, 그라디언트, 애니메이션)을 구현하기 위해 이 메소드를 오버라이드합니다. Canvas 매개변수는 모든 그림 작업이 수행되는 그래픽 표면입니다.

메소드 서명은 간단합니다: override fun onDraw(canvas: Canvas). 이 메소드 내에서는 (GC 부하를 피하기 위해) 새 객체를 만들거나, 긴 작업을 수행하거나, 네트워크에 액세스하지 않아야 합니다. View는 수명 주기에서 onLayout(위치 계산)과 dispatchDraw(자식 요소 그림) 사이에 onDraw 메소드도 포함합니다.

View를 상속할 때, 콘텐츠를 표시하려면 onDraw 오버라이드가 필수입니다 — 그렇지 않으면 View가 비어 있습니다. 기존 하위 클래스(TextView, ImageView, Button)를 상속할 때, onDraw 오버라이드는 선택적이며 표준 콘텐츠 위에 추가 그래픽을 덧붙이는 데 사용됩니다.

View 그림 사이클

Android는 메인 스레드(UI 스레드)에서 호출되는 엄격하게 정의된 순서로 View 그림을 수행합니다. 전체 사이클은 세 단계로 구성됩니다: measure(차원 측정), layout(위치 계산), draw(렌더링). onDraw는 draw 단계의 일부로, View의 차원과 위치가 결정된 후에 호출됩니다.

Draw 단계: dispatchDraw 및 onDraw

draw(Canvas canvas)는 onDraw와 dispatchDraw를 호출하는 View의 공용 메소드입니다. draw 메소드는 오버라이드용으로 설계되지 않았습니다 — 대신 onDraw가 오버라이드됩니다. 현재 View에서 onDraw가 실행된 후, dispatchDraw가 호출되며, 이는 모든 자식 View(현재 View가 ViewGroup 컨테이너인 경우)에 대해 재귀적으로 draw를 호출합니다.

시스템이 onDraw를 호출하는 시기

onDraw다음 조건에서 호출됩니다: 1) View가 처음으로 보이게 될 때(setVisibility(VISIBLE) 이후); 2) 앱이 이 View 또는 그 부모에서 invalidate()를 호출할 때; 3) View의 크기가 바됐을 때(새 layout); 4) View와 관련된 애니메이션이 바됐을 때(ValueAnimator 또는 ObjectAnimator 통해); 5) View의 상태가 바됐을 때(pressed, focused, enabled).

하드웨어 가속

Android 3.0 (API 11)부터, 대부분의 Canvas 작업이 GPU를 통해 하드웨어 가속됩니다. 개발자는 canvas.isHardwareAccelerated()를 통해 가속 사용 여부를 확인할 수 있습니다. 하드웨어 가속에서는 일부 Canvas 작업(예: 비직사각 영역에서의 clipPath)이 지원되지 않으며, 무시되거나 오류를 일으큼 수 있습니다.

커스텀 그래픽을 위한 Canvas API

Canvas는 다양한 그림 메소드 세트를 제공합니다. 모든 메소드는 변환(translate, rotate, scale, skew)으로 변경할 수 있는 현재 좌표계에서 작동합니다. 각 메소드는 색상, 스타일, 선 두께, 효과 및 안티에일리싱을 정의하는 Paint 객체를 수늙니다.

Canvas 메소드목적주요 Paint 매개변수
drawCircle원 그리기centerX, centerY, radius, paint
drawRect직사각형 그리기left, top, right, bottom, paint
drawLine선 그리기startX, startY, endX, endY, paint
drawPath임의의 경로 그리기path, paint
drawBitmap비트맵 이미지 그리기bitmap, srcRect, dstRect, paint
drawText텍스트 그리기text, x, y, paint
drawArc호 또는 섹터 그리기ovalRect, startAngle, sweepAngle, useCenter, paint
drawOval타원 그리기ovalRect, paint

Paint 객체

Paint는 그림 스타일을 정의합니다: 색상(setColor), 선 두께(setStrokeWidth), 채우기 스타일(setStyle — FILL, STROKE, FILL_AND_STROKE), 안티에일리싱(setAntiAlias), 효과(setShadowLayer, setMaskFilter) 및 그라디언트(setShader). Paint은 onDraw 내가 아닌 View 생성자에서 한 번 만들고 구성할 것을 권장합니다.

좌표 및 변환

Canvas는 X축이 오른쪽으로, Y축이 아래쪽으로 향하는 좌표계를 사용합니다. 원점 (0, 0)은 View의 왼쪽 위 모서리입니다. draw 메소드를 호출하기 전에 개발자는 canvas.translate(dx, dy), canvas.rotate(degrees, px, py), canvas.scale(sx, sy, px, py)를 통해 좌표계를 변경할 수 있습니다. 그 후의 모든 그림 작업은 변환된 시스템에서 수행됩니다.

onDraw 성능

onDraw는 메인 UI 스레드에서 실행되며, 그 지속 시간은 프레임 속도(fps)에 직접 영향을 미칩니다. 60 fps를 유지하려면 onDraw 메소드가 16 밀리초 내에 완료되어야 합니다. 어떤 다이도 프레임 드롭(jank)을 유발하며, 이는 액니메이션 똑글날리임 또는 스크롤 지연으로 시각적으로 나타난니다.

onDraw 성능 규칙

주요 규칙 onDraw 최적화에 따른 것: 1) onDraw 내에서 객체를 만들지 마세요 — 모든 Paint, Path, Rect 객체는 클래스 필드여야 함; 2) 메모리를 할당(new)하지 마세요 — 이는 가비지 수집(GC)과 프레임 드롭을 유발함; 3) 복잡한 작업을 Bitmap.createBitmap으로 Bitmap에 캐시하고 준비된 Bitmap을 그리세요; 4) I/O 작업이나 리소스 로딩을 하지 마세요.

clipRect를 사용한 최적화

clipRect는 그림 영역을 지정된 직사각형으로 제한합니다. Canvas가 보이는 영역 밖의 픽셀을 그리는 데 시간을 낭비하지 않도록 하는 데 사용됩니다. 큰 리스트를 스크롤하거나 줌할 때 특히 효과적입니다: clipRect를 보이는 영역(dirty rect)으로 설정하면, Canvas는 보이는 것만 그립니다.

레이어 — 저장 및 복구

canvas.save()는 현재 Canvas 상태(변환 및 clip)를 스택에 저장합니다. canvas.restore()는 마지막으로 저장된 상태를 복구합니다. 이는 일시적인 변환에 필요합니다: 상태를 저장하고, translate/rotate를 적용하고, 그리고, 그런 다음 복구합니다. save와 restore는 가벼우며 적절히 사용(10–15 네스팅 레벨 미만)하면 성능에 영향을 미치지 않습니다.

invalidate 및 postInvalidate

invalidate()는 View 메소드로, View의 콘텐츠가 낮은 불편하며 재그림이 필요하다는 것을 시스템에 알립니다. invalidate를 호출하면 onDraw가 호출되는 새 draw 사이클이 시작됩니다. invalidate는 UI 스레드(메인 스레드)에서만 호출해야 합니다. 백그라운드 스레드에서 호출하려면 postInvalidate를 사용합니다.

postInvalidate()는 invalidate의 스레드 안전 버전으로, Handler를 통해 메인 스레드의 큐에 재그림 요청을 보냅니다. 데이터 로딩 또는 계산 완료 후 View를 업데이트해야 할 때 백그라운드 작업에서 사용됩니다. UI 동기화를 위해 runOnUiThread와 결합할 수 있습니다.

부분 재그림을 위해 invalidate(Rect dirty) 또는 invalidate(int l, int t, int r, int b)를 사용합니다. 이는 View 개체가 아니라 지정된 영역만 변경되었음을 시스템에 알립니다. Android는 dirty rect만 재그림하여 렌더링을 최적화하며, 부분 업데이트가 있는 애니메이션의 성능을 현저히 향상시킵니다.

Kotlin 코드 예제

Kotlin을 사용하여 Android에서 커스텀 View를 만드는 onDraw 오버라이드의 실제 예를 보겠습니다.

커스텀 View — 원형 진행

이 예제는 애니메이션된 채우기가 있는 원형 진행 표시기를 만듭니다. onDraw는 두 개의 원을 그립니다: 회색 배경 원과 현재 진행에 따라 호가 결정되는 색칠된 채우기 원. setProgress에서 invalidate를 호출하면 값이 바됐을 때 재그림이 보장됩니다.

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

    private val bgPaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeWidth = 12f
        color = Color.parseColor("#E0E0E0")
        isAntiAlias = true
    }

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

    var progress: Float = 0f
        set(value) {
            field = value.coerceIn(0f, 1f)
            invalidate()
        }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val size = minOf(width, height) - 24f
        val oval = RectF(12f, 12f,
                          12f + size, 12f + size)

        canvas.drawArc(oval, 270f, 360f, false, bgPaint)
        canvas.drawArc(oval, 270f,
                        progress * 360f,
                        false, progressPaint)
    }
}

커스텀 View — 텍스트가 있는 그라디언트 배경

이 예제는 그라디언트가 있는 직사각형과 그 위에 텍스트를 그립니다. 그라디언트는 LinearGradient(Shader)를 통해 만들어지고, 텍스트는 drawText를 통해 그려집니다. 둘다 Paint 객체는 생성자에서 한 번 만들어지고, onDraw는 View의 현재 너비에 따라 직사각형 크기를 업데이트할 뿐입니다.

kotlin
class GradientTextView(context: Context)
    : View(context) {

    private val shaderPaint = Paint().apply {
        isAntiAlias = true
    }

    private val textPaint = Paint().apply {
        color = Color.WHITE
        textSize = 48f
        isAntiAlias = true
        textAlign = Paint.Align.CENTER
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val gradient = LinearGradient(
            0f, 0f, width.toFloat(), 0f,
            Color.parseColor("#FF6B35"),
            Color.parseColor("#FF0040"),
            Shader.TileMode.CLAMP
        )
        shaderPaint.shader = gradient
        canvas.drawRect(0f, 0f,
                        width.toFloat(),
                        height.toFloat(), shaderPaint)

        val xCenter = width / 2f
        val yCenter = height / 2f
        val yOffset = (textPaint.descent() +
                        textPaint.ascent()) / 2f

        canvas.drawText("Hello from Canvas",
                      xCenter, yCenter - yOffset,
                      textPaint)
    }
}

invalidate를 사용한 애니메이션 시계

이 예제는 1000 밀리초마다 그림을 업데이트하는 애니메이션 시계를 만듭니다. onDraw 내의 postDelayed는 1초 후 invalidate를 호출하여 무한 재그림 루프를 만듭니다. 시계 축은 다른 길이와 두께로 drawLine을 통해 그려집니다.

kotlin
class AnalogClockView(context: Context)
    : View(context) {

    private val handPaint = Paint().apply {
        style = Paint.Style.STROKE
        isAntiAlias = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val cx = width / 2f
        val cy = height / 2f
        val radius = minOf(cx, cy) - 20f

        val now = Calendar.getInstance()
        val hours = now.get(Calendar.HOUR) % 12
        val minutes = now.get(Calendar.MINUTE)
        val seconds = now.get(Calendar.SECOND)

        val secondAngle = Math.toRadians(
            seconds * 6f - 90f)
        handPaint.strokeWidth = 2f
        handPaint.color = Color.RED
        canvas.drawLine(cx, cy,
            cx + cos(secondAngle) * radius * 0.8f,
            cy + sin(secondAngle) * radius * 0.8f,
            handPaint)

        postDelayed({ invalidate() }, 1000)
    }
}

자주 묻는 질문

onDraw를 직접 호출할 수 있나요?

아니요, onDraw를 직접 호출하면 안 됩니다. 재그림을 요청하려면 invalidate()(UI 스레드에서) 또는 postInvalidate()(백그라운드 스레드에서)를 사용하세요. 시스템이 onDraw를 호출할 시기를 결정하고, 여러 invalidate 호출을 하나의 패스로 합치여 그림을 최적화합니다.

invalidate 후에 onDraw가 호출되지 않는 이유는?

이유: View가 숨겨져 있음(setVisibility(GONE) 또는 INVISIBLE); View가 Window 계층에 추가되지 않음; View의 크기가 0(너비 또는 높이 = 0); setWillNotDraw(true)가 호출됨(ViewGroup에서 기본값 true). 디버깅 시 이 각 항목을 확인하세요.

onDraw는 얼마나 자주 호출될 수 있나요?

60 fps에서 onDraw는 최대 초당 60번 호출될 수 있습니다. 120 fps(120 Hz 디스플레이 장치)에서는 최대 120번까지 호출될 수 있습니다. 각 호출은 약 8–16 밀리초 내에 완료되어야 합니다. ObjectAnimator 또는 ValueAnimator를 통한 활성적인 애니메이션 중에는 빈도가 애니메이션 프레임 수에 의해 결정됩니다.

onDraw와 dispatchDraw의 차이점은?

onDraw는 View 자체의 콘텐츠를 그리는 역할을 합니다. dispatchDraw는 자식 View(ViewGroup 내)를 그리는 역할을 합니다. dispatchDraw는 onDraw 후에 호출되며 모든 자식 요소를 재귀적으로 포관합니다. 일반 View(ViewGroup 이외)에서 dispatchDraw는 비어 있습니다.

Canvas를 onDraw 밖에서 사용할 수 있나요?

, Canvas(bitmap)으로 Bitmap에 대한 Canvas를 만들고 어떤 스레드에서나 그릴 수 있습니다. 이는 백그라운드에서 복잡한 그래픽을 미리 렌더링하는 데 사용됩니다. 그러나 onDraw에 전달된 Canvas는 시스템에 속하며 onDraw 메소드 내의 UI 스레드에서만 사용할 수 있습니다.

요약

  • onDraw(Canvas) — Android에서 Canvas API를 사용한 커스텀 그림을 위한 주요 View 메소드
  • 그림 사이클은 measure, layout, draw 단계로 구성 — onDraw는 draw 단계에서 호출됨
  • Canvas API에는 drawCircle, drawRect, drawPath, drawBitmap, drawText 등이 포함됨
  • invalidate()는 UI 스레드에서 재그림을 요청하고, postInvalidate()는 백그라운드 스레드에서 요청함
  • 부분 재그림 invalidate(Rect)를 통해 dirty 영역만 업데이트하여 렌더링을 최적화
  • onDraw 성능은 중요: 메소드 내 객체 생성은 GC와 프레임 드롭을 유발함
  • 하드웨어 가속(API 11부터) GPU를 통해 Canvas 작업을 자동으로 가속함

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

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

프로젝트 논의

더 읽어보기