커스텀 위젯 설계 — RenderObject와 렌더링 파이프라인
커스텀 위젯 설계 — RenderObject와 렌더링 파이프라인
Flutter의 위젯은 내부적으로 3단계 트리를 거쳐 화면에 그려집니다. 대부분은 기존 위젯 조합으로 해결되지만, 성능 최적화나 완전히 새로운 레이아웃이 필요할 때 RenderObject를 직접 다뤄야 합니다.
렌더링 파이프라인
Widget → Element → RenderObject → Layer → Skia(엔진) → 화면
상세 과정
- Build Phase: Widget 트리 생성/갱신
- Layout Phase: RenderObject가 크기와 위치 계산
- Paint Phase: RenderObject가 Canvas에 그리기
- Composite Phase: Layer를 합성하여 최종 이미지 생성
3개의 트리 복습
// Widget: 설정/구성 정보 (불변, 가벼움)
const Text('안녕') // 매 빌드마다 새로 생성 가능
// Element: Widget과 RenderObject의 다리 (상태 유지)
// - Widget이 바뀌었을 때 RenderObject를 재사용할지 결정
// - BuildContext가 사실 Element
// RenderObject: 실제 레이아웃과 페인팅 (무거움)
// - 크기 계산 (performLayout)
// - 화면 그리기 (paint)
면접 포인트: "Widget을 매번 새로 만드는데 성능 문제가 없나요?"라는 질문에 대한 답. Widget은 가벼운 설정 객체일 뿐이고, 실제 무거운 RenderObject는 Element가 재사용 여부를 판단하여 최적화합니다.
LeafRenderObjectWidget — 자식 없는 커스텀 위젯
// 단순한 원을 그리는 커스텀 위젯
class DotWidget extends LeafRenderObjectWidget {
final Color color;
final double size;
const DotWidget({
super.key,
this.color = Colors.blue,
this.size = 20,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderDot(color: color, dotSize: size);
}
@override
void updateRenderObject(BuildContext context, RenderDot renderObject) {
renderObject
..color = color
..dotSize = size;
}
}
class RenderDot extends RenderBox {
Color _color;
double _dotSize;
RenderDot({required Color color, required double dotSize})
: _color = color,
_dotSize = dotSize;
Color get color => _color;
set color(Color value) {
if (_color == value) return;
_color = value;
markNeedsPaint(); // 다시 그리기 요청
}
double get dotSize => _dotSize;
set dotSize(double value) {
if (_dotSize == value) return;
_dotSize = value;
markNeedsLayout(); // 다시 레이아웃 요청
}
@override
void performLayout() {
// 크기 결정
size = constraints.constrain(Size(_dotSize, _dotSize));
}
@override
void paint(PaintingContext context, Offset offset) {
final paint = Paint()..color = _color;
final center = offset + Offset(size.width / 2, size.height / 2);
context.canvas.drawCircle(center, size.width / 2, paint);
}
}
// 사용
DotWidget(color: Colors.red, size: 30)
SingleChildRenderObjectWidget — 자식 1개
class CustomPadding extends SingleChildRenderObjectWidget {
final double padding;
const CustomPadding({
super.key,
required this.padding,
required super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomPadding(padding: padding);
}
@override
void updateRenderObject(
BuildContext context,
RenderCustomPadding renderObject,
) {
renderObject.padding = padding;
}
}
class RenderCustomPadding extends RenderProxyBox {
double _padding;
RenderCustomPadding({required double padding}) : _padding = padding;
double get padding => _padding;
set padding(double value) {
if (_padding == value) return;
_padding = value;
markNeedsLayout();
}
@override
void performLayout() {
if (child != null) {
child!.layout(
constraints.deflate(EdgeInsets.all(_padding)),
parentUsesSize: true,
);
size = constraints.constrain(
Size(
child!.size.width + _padding * 2,
child!.size.height + _padding * 2,
),
);
} else {
size = constraints.constrain(Size.zero);
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(
child!,
offset + Offset(_padding, _padding),
);
}
}
}
Constraints와 Layout 프로토콜
Flutter의 레이아웃은 2단계 패스 로 동작합니다.
부모 → 자식: Constraints (제약) 전달
자식 → 부모: Size (크기) 보고
"Constraints go down. Sizes go up. Parent sets position."
@override
void performLayout() {
// 1. 자식에게 제약 전달하며 레이아웃 요청
child!.layout(
BoxConstraints(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight / 2,
),
parentUsesSize: true, // 자식 크기를 사용할지
);
// 2. 자신의 크기 결정
size = Size(
constraints.maxWidth,
child!.size.height + 20,
);
}
markNeedsLayout vs markNeedsPaint
| 메서드 | 호출 시점 | 비용 |
|---|---|---|
markNeedsLayout() | 크기/위치가 변할 때 | 높음 (레이아웃 + 페인트) |
markNeedsPaint() | 색상/스타일만 변할 때 | 낮음 (페인트만) |
성능을 위해 가능하면 markNeedsPaint()만 호출하세요.
언제 RenderObject를 직접 만들어야 할까?
| 상황 | 방법 |
|---|---|
| 기존 위젯 조합으로 가능 | StatelessWidget/StatefulWidget |
| 커스텀 그리기 필요 | CustomPainter |
| 새로운 레이아웃 알고리즘 | RenderObject |
| 극한의 성능 최적화 | RenderObject |
대부분의 경우 CustomPainter로 충분합니다. RenderObject는 정말 특별한 경우에만 사용하세요.
정리
- Flutter는 Widget → Element → RenderObject → Layer 순으로 렌더링합니다
- Widget은 가벼운 설정 객체, RenderObject가 실제 레이아웃과 페인팅을 담당합니다
- "Constraints go down, Sizes go up, Parent sets position" 규칙을 기억하세요
markNeedsLayout()은 비용이 높으니, 가능하면markNeedsPaint()를 사용하세요- 대부분은 CustomPainter로 충분하고, RenderObject는 특수한 경우에만 필요합니다
댓글 로딩 중...