커스텀 뷰는 모든 모바일 앱의 고유한 인터페이스의 기초입니다. 모바일 앱의 커스텀 뷰를 사용하면 표준 컴포넌트에서 사용할 수 없는 고유한 그래픽, 애니메이션 및 요소를 만들 수 있습니다. Apple Developer, 2025에 따르면, drawRect는 iOS 모바일 개발에서 커스텀 그리기의 주요 메서드로 남아 있습니다. Android에서는 Canvas와 함께 onDraw가 동일한 역할을 수행하며, Flutter에서는 CustomPainter가 그 역할을 합니다.
핵심 포인트
커스텀 뷰는 고유한 그래픽을 만들기 위해 표준 그리기 및 측정 메서드를 재정의하는 사용자 정의 인터페이스 컴포넌트입니다. 차트, 애니메이션, 다이어그램, 커스텀 채트 요소 및 AR 마스크 — 이것들은 모바일 앱에서 커스텀 뷰의 일반적인 사용 사례입니다. 표준 UI 컴포넌트(Button, TextView, Label)는 80%의 작업을 커버하지만, 나머지 20%는 모바일 개발에서 그리기에 대한 완전한 제어가 필요합니다. 모바일 개발에서 그리기는 주니어 개발자와 시니어 개발자를 구분하는 기술입니다.
모바일 앱의 커스텀 뷰는 고유한 2D 그래픽, 복잡한 애니메이션, 차트 및 데이터 시각화에 필요합니다. 표준 UIKit이 필요한 요소를 제공하지 않는 경우 — 커스텀 뷰가 생성됩니다. 모바일 앱에서 커스텀 뷰는 진행 표시줄, 커스텀 버튼, 그라데이션 채우기, 애니메이션 아이콘 및 손가락 그리기 캔버스에 사용됩니다. 과도하게 사용하지 않는 것이 중요합니다 — 간단한 스타일의 경우 Appearance API를 통해 표준 컴포넌트를 구성하는 것으로 충분합니다.
iOS에서 커스텀 뷰는 UIView를 상속하고 drawRect 메서드를 재정의하여 생성됩니다. drawRect는 첫 번째 렌더링 시와 각 setNeedsDisplay() 호출 후에 호출됩니다. drawRect 내부에서 개발자는 UIGraphicsGetCurrentContext()를 통해 CGContext를 얻습니다 — 선, 도형, 그라데이션 및 텍스트를 위한 캔버스입니다. Core Graphics(Quartz 2D)는 모든 픽셀을 완전히 제어할 수 있는 저수준 CPU 기반 렌더링 엔진입니다. UIBezierPath는 간결한 구문으로 경로를 생성하기 위한 편리한 래퍼입니다.
class CircleView: UIView {
private var fillColor: UIColor = .systemBlue
override func drawRect(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setFillColor(fillColor.cgColor)
ctx.addEllipse(in: bounds.insetBy(dx: 4, dy: 4))
ctx.drawPath(using: .fill)
}
func updateColor(_ newColor: UIColor) {
fillColor = newColor
setNeedsDisplay()
}
}
Core Graphics는 모든 그리기 작업을 위한 캔버스인 CGContext를 제공합니다. CGGradient는 CGColorSpace(sRGB, Display P3)를 지원하여 선형 및 방사형 그라데이션을 생성합니다. 변환은 CGAffineTransform을 통해 수행됩니다 — 회전, 크기 조정 및 이동을 위한 3x3 행렬입니다. CGContextTranslateCTM, CGContextRotateCTM 및 CGContextScaleCTM은 좌표계를 수정합니다. Core Graphics는 CPU에서 실행되므로 복잡한 그래픽은 프로세서에 부하를 줄 수 있습니다 — GPU 가속을 위해 Metal을 사용하세요.
UIBezierPath는 CGPath 위의 객체 지향 래퍼입니다. addArc(withCenter:radius:startAngle:endAngle:clockwise:) 메서드는 호와 원을 생성하고, addCurve는 베지어 곡선을 생성합니다. 경로는 addClip을 통해 합집합, 교집합 및 차집합 연산을 지원합니다. UIBezierPath는 커스텀 마스크(maskView), 클리핑 경로 및 복잡한 도형 — 별, 하트, 커스텀 아이콘에 편리합니다.
Android에서 커스텀 뷰는 onDraw(Canvas)를 재정의하여 View를 확장합니다. Canvas에는 모바일 개발의 모든 그리기 메서드가 포함되어 있습니다: drawCircle, drawRect, drawLine, drawText, drawBitmap, drawPath 및 drawRoundRect. Paint는 매개변수 — 색상, 선 두께, 스타일(FILL, STROKE, FILL_AND_STROKE), 앤티앨리어싱(isAntiAlias) 및 텍스트 크기를 구성합니다. onDraw는 매 invalidate() 시 호출되며 무거운 작업을 포함해서는 안 됩니다 — 모든 계산은 캐시됩니다.
class CustomCircleView(context: Context, attrs: AttributeSet?)
: View(context, attrs) {
private val paint = Paint().apply {
style = Paint.Style.FILL
color = Color.BLUE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 4f
canvas.drawCircle(cx, cy, radius, paint)
}
}
Android의 Path는 UIBezierPath와 동등합니다: addCircle, addRect, addOval, lineTo, cubicTo 및 quadTo. PathMeasure는 경로 길이와 임의 지점의 좌표를 계산합니다 — 경로를 따라 객체의 움직임을 애니메이션하는 데 유용합니다. 애니메이션의 경우 콜백에서 invalidate()와 함께 ValueAnimator를 사용합니다. 애니메이터는 색상, 반경, 위치를 변경할 수 있습니다 — 그리기에 영향을 미치는 모든 속성입니다. Android의 커스텀 뷰는 XML을 통한 애니메이션을 위해 StateListDrawable 및 animated-vector도 지원합니다.
Android의 그라데이션은 Shader를 통해 설정됩니다 — LinearGradient, RadialGradient 및 SweepGradient. Shader는 paint.setShader(shader)를 통해 Paint에 할당됩니다. LinearGradient는 두 점 사이에 선택적 색상 위치가 있는 선형 그라데이션을 생성합니다. RadialGradient — 중심에서 가장자리로 방사형. SweepGradient — 중심점 주위의 원뿔형. Shader.TileMode(CLAMP, REPEAT, MIRROR)는 영역 경계를 넘는 동작을 정의합니다.
Flutter는 CustomPainter를 사용합니다 — paint(Canvas canvas, Size size) 및 shouldRepaint 메서드가 있는 클래스입니다. Flutter의 Canvas는 Android와 동일한 메서드를 제공합니다: drawCircle, drawRect, drawLine, drawPath, drawParagraph 및 drawImage. Flutter의 Paint는 색상, 두께, 스타일 및 앤티앨리어싱을 구성합니다. Flutter는 drawRect나 onDraw를 사용하지 않습니다 — 모든 그리기는 CustomPaint 위젯에 내장된 CustomPainter를 통해 이루어집니다.
class CirclePainter extends CustomPainter {
final Color color;
CirclePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final radius = size.shortestSide / 2 - 4;
canvas.drawCircle(size.center(Offset.zero), radius, paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
oldDelegate.color != color;
}
shouldRepaint는 CustomPainter를 다시 그려야 하는지 여부를 결정합니다. 렌더링에 영향을 미치는 데이터가 변경된 경우에만 true를 반환합니다 — 색상, 크기, 차트 데이터. RepaintBoundary는 다시 그리기를 별도 레이어로 격리합니다 — 드물게 변경되는 위젯에 사용합니다. Flutter에는 setNeedsDisplay나 invalidate가 없습니다 — 다시 그리기는 setState 또는 변경 사항을 수신하는 ValueNotifier를 통해 트리거됩니다.
Flutter는 LinearGradient, RadialGradient 및 SweepGradient를 지원합니다 — Android와 동일한 유형입니다. Gradient는 createShader(Rect bounds)를 통해 Paint의 Shader를 생성합니다. 변환은 canvas.rotate, canvas.scale, canvas.translate 및 canvas.skew를 통해 수행됩니다. Flutter는 Transform 위젯에 4x4 변환 행렬(Matrix4)을 사용합니다. Flutter의 그리기 애니메이션은 CustomPainter를 다시 그리기 위해 setState와 함께 AnimationController를 사용하여 만들어집니다.
커스텀 뷰의 올바른 측정은 예측 가능한 인터페이스 동작의 기초입니다. iOS에서 뷰의 크기는 sizeThatFits 및 intrinsicContentSize를 통해 결정됩니다. sizeThatFits는 주어진 제약 조건에 대한 최적의 크기를 반환합니다. intrinsicContentSize — 외부 제약 조건이 없는 콘텐츠의 자연스러운 크기입니다. Android에서 측정은 MeasureSpec 매개변수(UNSPECIFIED, EXACTLY, AT_MOST)와 함께 onMeasure에서 구현됩니다. Flutter에서 크기는 paint(Canvas, Size)로 전달되며 computeDryLayout 또는 LayoutBuilder를 통해 계산할 수 있습니다.
intrinsicContentSize는 CGSize를 반환합니다 — 콘텐츠를 기반으로 한 뷰의 자연스러운 크기입니다. setContentHuggingPriority 및 setContentCompressionResistancePriority는 늘어남과 압축 시 동작을 제어합니다. sizeThatFits는 Auto Layout에서 크기를 계산하기 위해 시스템에 의해 호출됩니다. 커스텀 뷰의 경우 컴포넌트에 자연스러운 크기가 있으면 intrinsicContentSize를 재정의합니다 — 예를 들어, 원은 항상 100x100이어야 합니다.
onMeasure는 widthMeasureSpec 및 heightMeasureSpec을 허용합니다. MeasureSpec에는 모드와 크기가 포함됩니다: UNSPECIFIED(제약 없음, 뷰가 자체 크기를 선택), EXACTLY(부모의 정확한 크기), AT_MOST(최대 크기). setMeasuredDimension(width, height)는 측정 결과를 저장합니다. 데이터가 변경되면 재측정을 위해 requestLayout()을 호출합니다. 중첩된 ViewGroup은 자식 요소를 배치하기 위해 onLayout을 재정의해야 합니다.
| 플랫폼 | 측정 메서드 | 변경 시 호출 | 반환 |
|---|---|---|---|
| iOS | sizeThatFits / intrinsicContentSize | setNeedsLayout / invalidateIntrinsicContentSize | CGSize |
| Android | onMeasure / setMeasuredDimension | requestLayout / invalidate | void (크기 저장) |
| Flutter | computeDryLayout | setState / LayoutBuilder | Size |
computeDryLayout는 전체 레이아웃 주기 없이 크기를 계산하기 위한 RenderObject 메서드입니다. LayoutBuilder.getSize — build 메서드에서 사용 가능한 공간을 얻기 위한 대안입니다. Flutter에서 캔버스 크기는 Size로 paint에 전달되며 별도 측정이 필요하지 않습니다 — CustomPainter는 전달된 경계 내에서 그립니다. 그러나 커스텀 RenderObject의 경우 performLayout 및 computeDryLayout을 재정의해야 합니다.
자주 묻는 질문
drawRect는 Core Graphics를 통한 커스텀 그리기를 위한 UIView 메서드입니다. 뷰의 첫 번째 렌더링 시와 각 setNeedsDisplay() 호출 후에 호출됩니다. 선, 도형 및 텍스트에 대해 drawRect 내에서 CGContext를 사용하세요.
View를 확장하는 클래스를 만들고, onDraw(Canvas canvas)를 재정의하고 Canvas API를 사용하세요: drawCircle, drawRect, drawPath. Paint는 색상, 두께 및 스타일을 구성합니다. 업데이트하려면 invalidate()를 호출하세요.
CustomPainter는 Canvas에 커스텀 그리기 위한 Flutter 클래스입니다. paint(Canvas, Size) 및 shouldRepaint 메서드를 구현하세요. CustomPaint 위젯을 통해 배치됩니다. Flutter에는 drawRect가 없습니다 — 모든 그리기는 CustomPainter를 통해 이루어집니다.
iOS에서는 sizeThatFits 및 intrinsicContentSize를 사용하세요. Android에서는 — MeasureSpec과 함께 onMeasure를 사용하세요. Flutter에서는 크기가 paint(Canvas, Size)로 전달되거나 computeDryLayout을 통해 계산됩니다. 각 플랫폼은 자체 측정 메서드를 사용합니다.
커스텀 뷰는 표준 UIKit 또는 View로 구현할 수 없는 고유한 그래픽, 애니메이션, 차트 및 요소에 필요합니다. 간단한 스타일의 경우 표준 컴포넌트를 구성하는 것으로 충분합니다 — 불필요하게 아키텍처를 복잡하게 만들지 마세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.