blog

5. Flutter 학습하기 - 렌더 오브젝트 레이아웃 프로세스

레이아웃 프로세스 상자 제약 조건은 최대 및 최소 너비와 높이 제한을 설명합니다. 레이아웃 프로세스 중에 컴포넌트는 제약 조건을 통해 자신 또는 자식 컴포넌트의 크기를 결정합니다...

Oct 18, 2025 · 10 min. read
シェア

Flutter를 배우는 과정을 기록하는 일련의 글을 통해 관련 지식 포인트를 요약하세요.

BoxConstraints

상자 제약 조건은 주로 최대 및 최소 너비와 높이 제한을 설명합니다. 레이아웃 프로세스 중에 컴포넌트는 제약 조건을 통해 자신 또는 자식 노드의 크기를 결정합니다. 상자 제약 조건에는 최대/최소 너비, 최대/최소 높이의 네 가지 속성이 있으며, 이 네 가지 속성의 조합에 따라 다른 제약 조건도 구성됩니다. 먼저 그 구성 방법을 살펴보겠습니다.

box.dart 
 /// Creates box constraints with the given constraints.
 const BoxConstraints({
 this.minWidth = 0.0,
 this.maxWidth = double.infinity,
 this.minHeight = 0.0,
 this.maxHeight = double.infinity,
 })

위젯은 자식에게 특정 크기여야 한다고 말할 때 엄격한 제약 조건을 적용한다고 합니다.

엄격한 제약 조건

엄격하게 제한되어 정확한 크기가 주어지면 너비와 높이에 대해 최대 = 최소로 자식 노드로 전달됩니다;

box.dart 
/// Creates box constraints that is respected only by the given size.
BoxConstraints.tight(Size size)
 : minWidth = size.width,
 maxWidth = size.width,
 minHeight = size.height,
 maxHeight = size.height;

느슨한 제약 조건

주어진 너비와 높이가 간격인 것으로 해석할 수 있는 느슨한 제약 조건은 너비와 높이 값이 불확실한 자식 노드에 전달됩니다.

box.dart 
	/// Creates box constraints that forbid sizes larger than the given size.
 BoxConstraints.loose(Size size)
 : minWidth = 0.0,
 maxWidth = size.width,
 minHeight = 0.0,
 maxHeight = size.height;

레이아웃 프로세스

원리

Flutter에서 컴포넌트의 레이아웃은 RenderObject 객체를 통해 이루어지며, 레이아웃 프로세스는 주로 각 컴포넌트의 위치와 크기를 결정하는 것으로, 레이아웃 프로세스는 그림과 같이 우선순위 레이아웃 하위 노드인 RenderObject 트리 프로세스의 우선순위 탐색 깊이, 상위 노드의 레이아웃에서 이루어집니다:

기본 프로세스는 다음과 같습니다.

  1. 부모 노드는 자식 노드에 제약 조건을 전달하여 자식 노드의 최대 및 최소 너비와 높이를 제한합니다;

  2. 자식 노드는 제약 조건 정보를 기반으로 자신의 크기를 결정하고, 자식 노드의 크기는 부모 노드에서 사용할 수 있는 레이아웃 결과로 사용됩니다;

  3. 부모 노드는 특정 레이아웃 규칙에 따라 부모의 레이아웃 공간에서 각 자식 노드의 위치를 결정합니다.

레이아웃 경계는 무엇인가요?

레이아웃 경계의 역할

결론: 불필요한 노드의 릴레이 아웃을 피하세요.

아시다시피 Flutter의 레이아웃 프로세스는 RenderObject Tree의 각 노드를 깊이 탐색해야 합니다. 자식 노드를 릴레이 아웃해야 하는 경우 RenderObject Tree의 각 노드를 다시 탐색하는 것은 분명히 성능 낭비이며 불필요한 작업입니다. 따라서 릴레이아웃이 필요한 노드를 가장 작은 범위에서 제어하여 릴레이아웃Boundary의 부모 노드로 더 이상 전파되지 않도록 하고, 다음 프레임이 새로 고쳐지면 릴레이아웃Boundary의 부모 노드는 릴레이아웃이 필요하지 않도록 하는 것이 릴레이아웃Boundary의 역할이며, 이는 Flutter의 중요한 최적화 조치입니다. 이것은 Flutter에서 릴레이아웃의 중요한 최적화입니다.

그림과 같은 예를 살펴보세요:

모든 렌더 오브젝트에는 레이아웃 경계 노드를 가리키는 _relayoutBoundary 속성이 있으며, 현재 노드의 레이아웃이 변경되면 해당 노드에서 레이아웃 경계 노드까지의 경로에 있는 모든 노드가 릴레이아웃되어야 합니다.

  • 노드 R3을 재배치해야 하는 경우, R3의 릴레이아웃경계는 R1이며, 결국 R1과 R3 두 노드만 재배치하면 됩니다;

  • R5 노드를 재배치해야 하는 경우, R5의 릴레이아웃바운더리는 그 자체이며, 궁극적으로 재배치해야 하는 유일한 대상은 R5 자체입니다;

레이아웃 경계가 되기 위한 조건

릴레이아웃바운더리 섹션의 소스 코드부터 살펴보겠습니다:

object.dart
 
 void layout(Constraints constraints, { bool parentUsesSize = false }) {
 		//레이아웃 경계가 CustomAlign 컴포넌트인지 여부를 결정하는 조건에는 4가지가 있습니다.
 final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
 	//포인트가 이미 레이아웃 경계인 경우,_relayoutBoundary 은 자신을 가리키고, 그렇지 않으면 부모 노드의_relayoutBoundary
 final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
 		...
 _relayoutBoundary = relayoutBoundary;
 	...
 }

소스 코드에서 앞의 네 가지 조건 중 하나가 충족되면 레이아웃 경계가 된다는 것을 알 수 있습니다.

  • !parentUsesSize

부모 노드가 현재 노드의 크기를 레이아웃에 사용하지 않고 현재 노드가 레이아웃 경계가 될 수 있음을 의미합니다.

  • sizedByParent

sizedByParent = true이면 현재 노드의 크기는 부모 노드에서 전달된 제약 조건에만 의존하고 자식 노드의 크기에는 의존하지 않음을 의미하며, 현재 노드는 레이아웃 경계가 될 수 있습니다.

  • constraints.isTight

부모에서 전달된 제약 조건은 엄격한 제약 조건으로, sizedByParent = true와 같은 효과를 내며, 크기가 제약 조건에 의해 고유하게 결정되면 현재 노드는 레이아웃 경계가 될 수 있습니다.

  • constraints.isTight

부모 노드의 유형이 렌더객체 유형이 아닌 경우, 현재 노드는 레이아웃 경계가 될 수 있습니다.

마크니즈 레이아웃()

메서드의 동원 타이밍이 있습니다:

  • 렌더 오브젝트가 렌더 트리에 추가됩니다.
  • 자식 노드 채택, 삭제, 이동
  • 렌더 오브젝트 자체도 레이아웃 관련 프로퍼티가 변경될 때 호출됩니다.

소스 코드:

object.dart - RenderObject  
 
	void markNeedsLayout() {
 	//레이아웃 경계가 비어 있는 경우
 if (_relayoutBoundary == null) {
 _needsLayout = true;//재정렬이 필요한 것으로 자체 표시
 if (parent != null) {
 markParentNeedsLayout();//현재 지점에서 레이아웃 경계 노드까지 경로의 모든 노드에 대해 재귀적으로 markNeedsLayout 메서드를 호출합니다.
 }
 return;
 }
 	//
 if (_relayoutBoundary != this) {
 markParentNeedsLayout(); //레이아웃 경계가 아닌 경우 위와 동일합니다.
 } else {
 _needsLayout = true;
 if (owner != null) {
 owner!._nodesNeedingLayout.add(this);//레이아웃 경계 노드를 PipelineOwner의_nodesNeedingLayout  
 owner!.requestVisualUpdate(); //프레임 업데이트 요청
 }
 }
 }

소스 코드를 살펴보면 메서드가 다음을 수행한다는 것을 알 수 있습니다:

  • 현재 노드에서 해당 릴레이아웃경계까지의 경로에 있는 모든 노드를 "레이아웃 필요"로 표시합니다;
  • 지정된 파이프라인 소유자 목록에 레이아웃 경계 노드를 추가하는 것을 관리합니다;
  • 마지막으로, 다시 그리기 프로세스 중에 "레이아웃 필요"로 표시된 노드는 다시 그리기 인스턴스를 통해 다시 그리기를 요청합니다;

참고: 불필요한 재배열을 피하기 위해 레이아웃이 필요한 모든 노드는 실시간으로 업데이트되는 대신 PipelineOwner를 통해 수집되어 다음 프레임 새로고침 시 일괄 처리됩니다.

다음은 레이아웃 프로세스의 세부 사항을 더 자세히 이해하기 위해 렌더 오브젝트의 관련 메서드에 대한 소스 코드 분석입니다.

layout()

레이아웃 메서드는 일반적으로 부모 노드를 통해 자식 노드의 레이아웃 메서드를 호출하여 레이아웃 업데이트를 수행하기 위해 레이아웃 업데이트를 수행하는 RenderObjcet의 주요 진입점입니다. 레이아웃은 일부 공용 로직을 수행하는 RenderObject에 정의된 템플릿 메서드이며 실제 레이아웃 로직은 다양한 서브클래스의 performLayout 메서드에 있습니다. 실제 레이아웃 로직은 각 서브클래스의 performLayout 메서드에 있습니다.

소스 코드:

object.dart
 
 void layout(Constraints constraints, { bool parentUsesSize = false }) {
 		//1.현재 컴포넌트의 레이아웃 경계 결정하기
 final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
 final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
 		//2.현재 컴포넌트가 재배치 대상으로 표시되지 않았고 부모 컴포넌트가 전달한 제약 조건이 변경되지 않은 경우, 재배치가 필요하지 않고 컴포넌트가 바로 반환됩니다.
 if (!_needsLayout && constraints == _constraints) {
 if (relayoutBoundary != _relayoutBoundary) {
 _relayoutBoundary = relayoutBoundary;
 visitChildren(_propagateRelayoutBoundaryToChild);
 }
 return;
 }
 _constraints = constraints;
 if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
 visitChildren(_cleanChildRelayoutBoundary);
 }
 _relayoutBoundary = relayoutBoundary;
		//3. sizedByParent = true 정렬 매개변수를 다른 값으로 설정하면 크기를 다시 계산해야 합니다.
 if (sizedByParent) {
 performResize();
 }
		//4.레이아웃 실행하기
 performLayout();
 
 _needsLayout = false;
 	//5.마커 다시 그리기
 markNeedsPaint();
 }

레이아웃 메서드는 여러 가지 작업을 수행합니다:

  1. 현재 컴포넌트의 레이아웃 경계를 결정합니다;

  2. 새 레이아웃이 필요한지 여부 결정하기

    1. 현재 컴포넌트가 릴레이가 필요한 것으로 표시되지 않았고 부모 컴포넌트가 전달한 제약 조건이 변경되지 않은 경우 릴레이가 필요하지 않으며 레이아웃 경계만 업데이트됩니다;
  3. sizedByParent = true이면 현재 컴포넌트 크기가 부모 컴포넌트가 전달한 제약 조건에 의해 결정되며, 서브클래스가 performResize를 재정의해야 함을 의미합니다;

  4. 하위 클래스에서 재정의해야 하는 레이아웃 메서드 performLayout을 수행합니다;

  5. 마커를 다시 그려야 합니다;

performResize()


@override
 void performResize() {
 // default behavior for subclasses that have sizedByParent = true
 size = computeDryLayout(constraints);
 }

performLayout()

shifted_box.dart - RenderPositionedBox
@override
 void performLayout() {
 final BoxConstraints constraints = this.constraints;
 final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
 final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
 if (child != null) {
 //1. 제약 조건을 전달하여 하위 컴포넌트를 레이아웃합니다.
 child!.layout(constraints.loosen(), parentUsesSize: true);
 //2. 자식 컴포넌트의 크기에 따라 컴포넌트의 크기 결정하기
 size = constraints.constrain(Size(
 shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
 shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
 ));
 //3. 부모 노드에서 자식 노드의 위치가 자식 노드에 저장됩니다..parentData in
 alignChild();
 } else {
 size = constraints.constrain(Size(
 shrinkWrapWidth ? 0.0 : double.infinity,
 shrinkWrapHeight ? 0.0 : double.infinity,
 ));
 }
 }
	//
 void alignChild() {
 _resolve();
 final BoxParentData childParentData = child!.parentData! as BoxParentData;
 childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
 }

방법론에는 세 가지 주요 단계가 있습니다:

  1. 하위 컴포넌트 레이아웃, 제약 조건 전달
  2. 하위 컴포넌트의 크기에 따라 자체적으로 크기를 조정합니다;
  3. 부모 노드에서 자식 노드의 위치를 child.parentData에 저장합니다.

주의:

  • 이 메서드는 레이아웃 메서드에 의해 호출되며, 릴레이아웃이 필요할 때 직접 performLayout을 호출하는 대신 호출해야 합니다.
  • 자식 노드에서 레이아웃을 호출할 때 자식 노드의 크기를 사용하려면 parentUsesSize 매개변수가 true여야 합니다.

사용자 지정 레이아웃 실습

사용자 정의 정렬 컴포넌트인 CustomAlign을 구현하는 기능과 시스템 정렬은 기본적으로 동일하며, 프로세스 레이아웃의 주요 데모와 관련 메서드의 구현을 보여줍니다.

위젯 정의하기

class CustomAlign extends SingleChildRenderObjectWidget {
 final Alignment alignment;
 const CustomAlign({ Key? key, required Widget child, this.alignment = Alignment.topLeft})
 : super(key: key, child: child);
 //커스텀 렌더 오브젝트를 반환합니다.
 @override
 RenderObject createRenderObject(BuildContext context) {
 return RenderCustomAlignObject(alignment: alignment);
 }
 //업데이트 메서드를 재정의하여 정렬 속성 업데이트하기
 @override
 void updateRenderObject(BuildContext context, covariant RenderCustomAlignObject renderObject) {
 renderObject.alignment = alignment;
 }
}

위젯에는 레이아웃 사용을 위한 정렬 매개변수가 있으므로 레이아웃 속성을 업데이트하려면 updateRenderObject 메서드를 재정의해야 합니다.

렌더 오브젝트 정의

그런 다음 커스텀 RenderOjbect 클래스 RenderCustomAlignObject를 구현하고, RenderOjbect를 직접 상속하는 경우 레이아웃 자체에 더 집중하기 위해 레이아웃 독립적인 메서드를 수동으로 구현해야 하며, 여기서는 RenderShiftedBox에서 상속하므로 다음을 다시 작성하면 됩니다. 이렇게 하면 수행 레이아웃 메서드를 재정의하고 해당 메서드에서 자식 노드 레이아웃 알고리즘을 구현하기만 하면 됩니다.


class RenderCustomAlignObject extends RenderShiftedBox {
 Alignment alignment;
 RenderCustomAlignObject({RenderBox? child, required this.alignment}): super(child);
 @override
 void performLayout() {
 ///super.performLayout(); 슈퍼 컴포넌트를 호출할 필요가 없습니다..performLayout()
 if (child == null) {
 ///자식이 없는 경우 공간을 차지하지 않음
 size = Size.zero;
 return;
 }
		//1.자식 컴포넌트의 레이아웃
 child?.layout(constraints.loosen(), //제약 조건 전달
 parentUsesSize: true); //parentUsesSize = true 하위 컴포넌트의 크기를 사용해야 함을 나타냅니다.
		
 //2.자식의 크기에 따라 자체 크기를 결정합니다.
 size = constraints.constrain(Size(
 constraints.maxWidth == double.infinity
 ? child!.size.width
 : double.infinity,
 constraints.maxHeight == double.infinity
 ? child!.size.height
 : double.infinity,
 ));
		
 //3.부모 노드 자체 크기와 자식의 크기를 기준으로 부모 노드에서 자식의 위치를 계산합니다.
 // 마지막으로 자식.parentData in
 BoxParentData parentData = child?.parentData as BoxParentData;
 parentData.offset = alignment.alongOffset(size - child!.size as Offset); //오프셋 설정
 }
 
}

레이아웃 프로세스는 코드에 설명된 대로 진행됩니다.

최종 페인팅 단계에서는 위의 레이아웃에서 계산된 오프셋을 사용합니다(RenderShiftedBox 클래스의 소스 코드에서 페인트 메서드를 참조하세요).

 @override
 void paint(PaintingContext context, Offset offset) {
 final RenderBox? child = this.child;
 if (child != null) {
 
 final BoxParentData childParentData = child.parentData! as BoxParentData;
 //자식 그리기
 //부모 노드의 오프셋에 자식 노드의 오프셋을 더한 값이 화면에서 자식 노드의 오프셋이 됩니다.
 context.paintChild(child, childParentData.offset + offset);
 }
 }

컴포넌트의

다음은 CustomAlign 컴포넌트가 어떻게 작동하는지 확인하는 테스트입니다.

 Widget build(BuildContext context) {
 return MediaQuery(
 data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
 child: Scaffold(
 backgroundColor: KimKidColor.eedCsCommonBackgroundGrayiii,
 appBar: AppBar(
 centerTitle: true,
 title: Text("CustomAlign"),
 ),
 body: Container(
 width: 400,
 height: 400,
 color: Colors.red,
 // CustomAlign...
 child: CustomAlign(
 alignment:Alignment.center,
 child: Container(
 child: Container(
 width: 100,
 height: 100,
 color: Colors.green,
 ),
 ),
 ),
 //... CustomAlign
 )
 ));
 }

효과

맞춤 정렬 매개변수의 값을 다르게 설정했을 때의 효과는 다음과 같습니다.

Read next

2024 최신 Win11 프로페셔널 워크스테이션 에디션 정품 인증 키

Windows 11 프로페셔널 워크스테이션 에디션은 최대 2TB의 메모리를 지원합니다. 이는 모든 Windows 11 버전에서 지원되는 최대 메모리 용량입니다. Windows 11 홈 및 프로페셔널 에디션의 최대 지원 메모리는 128GB입니다. 윈도우 1

Oct 18, 2025 · 1 min read