힙하게 가변적인 위젯의 크기 구하기

Ximya(심야)·2024년 5월 5일
9
post-thumbnail

Flutter로 UI 작업을 하다 보면 자식 위젯에 따라 가변적인 크기를 가지는 위젯의 크기를 측정해야 하는 경우가 종종 있습니다. 직장인들이 많이 이용하는 '블라인드' 앱을 예로 들어보겠습니다.

블라인드 앱의 회사 리뷰 페이지를 보면, 리뷰 항목이 일정 높이를 넘어가면 더보기 버튼이 나타나고, 일부 UI가 가려지는 형태를 볼 수 있습니다. 더보기 버튼을 누르면 가려진 내용이 나타나는데요. 만약 특정 높이를 초과하지 않으면 더보기 버튼은 보이지 않습니다.

이러한 리뷰 목록 항목은 크기가 고정되어 있지 않고 텍스트 데이터의 양과 디바이스의 너비에 따라 유동적으로 높이가 변합니다. 그렇기 때문에 이런 구성을 가지고 있는 UI를 구현하기 위해서는 위젯이 렌더링된 높이를 측정하고 특정 높이를 초과하는지에 따라 조건별로 더보기 버튼과 가려지는 형태의 UI를 구성할 필요가 있습니다.

그럼 어떻게 가변적인 위젯의 렌더링 크기를 측정할 수 있을까요? 이번 포스팅에서는 간단한 예제를 통해 위젯의 가변적인 크기를 측정하는 방법은 단계별로 알아보려고 합니다. 또한 다음 개념들을 다루면서 Flutter의 렌더링 원리에 대해 다루고 있습니다.

  • WidgetTree, ElementTree, RenderTree
  • BuildContext
  • RenderObject
  • addPostFrameCallback
  • NotificationListener

구현 목표

포스팅에서 다룰 예제를 간단히 살펴보겠습니다.

위 스크린샷은 영화 출연진들의 정보를 보여주는 간단한 페이지입니다. 제목 섹션의 Text 위젯과 출연진들의 정보를 보여주는 ExpansionTile로 구성된 ListView 위젯이 Column 안에 감싸져 있으며, 제목 Text 위젯에는 현재 Column 위젯의 높이를 보여줍니다.

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Height : ${0}',
                style: PretendardTextStyle.bold(
                  size: 24,
                  height: 37,
                  letterSpacing: -0.2,
                ),
              ),
              const SizedBox(height: 10),
              ListView.separated(
                physics: const NeverScrollableScrollPhysics(),
                padding: EdgeInsets.zero,
                shrinkWrap: true,
                itemCount: CastModel.castList.length,
                separatorBuilder: (_, __) => const SizedBox(height: 8),
                itemBuilder: (context, index) {
                  final item = CastModel.castList[index];
                  return ExpansionTile(
                    tilePadding: EdgeInsets.zero,
                    title: Row(
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(56 / 2),
                          child: CachedNetworkImage(
                            height: 56,
                            width: 56,
                            imageUrl: item.imgUrl,
                            fit: BoxFit.cover,
                          ),
                        ),
                        const SizedBox(width: 10),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              item.name,
                              style: AppTextStyle.title1,
                            ),
                            Text(
                              item.role,
                              style: AppTextStyle.body3.copyWith(
                                color: AppColor.gray02,
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                    children: <Widget>[
                      Text(
                        item.description,
                        style: AppTextStyle.body3,
                      ),
                    ],
                  );
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

ExpansionTile을 클릭하게 되면 위젯이 확장되며 출연진들에 대한 상세 정보를 보여주는데요. 이에 따라 위젯의 크기가 변화하므로, 이에 맞게 높이 값도 조절되어야한다는 조건이 붙습니다. 다음과 같은 요구사항을 정리해 볼 수 있겠습니다.

- 현재 렌더링된 위젯의 정확한 크기를 얻을 수 있어야 함.
- 크기가 측정되는 위젯에서 해당 값에 접근하여 조건별로 UI를 처리할 수 있어야 .
- 위젯의 크기가 동적으로 변할 때 이를 감지하고 변화된 크기를 얻을 수 있어야 함 (확장 가능한 크기 X)
- 사용이 편리하고 간단해야 함.

아래 링크를 통해 구현된 예제를 확인하실 수 있습니다.
👉 Measure Size Implementation


위젯이 어떻게 그려질까?

먼저, Flutter에서 위젯이 화면에 그려지는 원리를 살펴보죠.

Flutter는 Widget, Element, Render 이렇게 총 3가지의 트리 구조를 기반으로 위젯을 생성한다는 개념을 많이 들어보셨을 겁니다. Flutter로 개발 하는 과정에서 엘레먼트 또는 렌더트리의 객체들을 접할 경우가 비교적 많이 없기 때문에 익숙하지 않으실 수 있어요. 하지만 이번 예제처럼 엘러먼트 또는 렌더 객체의 직접적인 조작 및 접근이 필요한 경우 Flutter의 위젯 트리 구조에 대한 기본적인 개념을 숙지하시면 큰 도움이 됩니다. 그래서 조금 더 이해하기 쉽게 설명을 드려 볼까 합니다.

Widget Tree - 자동차 설계도면

class Lamborghini extends StatelessWidget {
  const Lamborigini({super.key});

  
  Widget build(BuildContext context) {
    return Car(
        paint: RedPaint(),
        engine: 4LV8Engine(),
        wheel: RimsAltaneroShinyBlack(),
        carbon : UpperExteriorCarbon(),
        ...
    );
  }
}

위젯의 세 가지 트리 구조에 대한 이해를 돕기 위해 람보르기니 차량을 만드는 과정에 비유해 보겠습니다. 차량을 만들기 위해서는 색상, 엔진 등 여러 구성 요소를 결정해야 합니다. 위 코드에서 build 메소드 내부에서 Car 클래스에 필요한 옵션들을 전달하고 있습니다.

이러한 과정은 차량의 설계 도면을 제작하는 과정과 유사합니다. 자동차의 부품들이 어떤 구조와 형태로 구성되는지 정의하는 것이죠. StatelessWidget 또는 StatefulWidget은 항상 build 메소드를 오버라이드하고, 내부에서는 Widget을 반환합니다. 이러한 코드들은 위젯 트리로 반환되며, 내부적으로 createElement()를 통해 필요한 Element를 생성합니다.

핵심 포인트
build 메소드 내부의 코드는 '위젯 트리'로 반환되어 '엘리먼트 트리'를 생성합니다.


Element Tree - 자동차 부품과 엔지니어

이렇게 위젯 트리로부터 생성된 엘러먼트 트리는, 위젯의 일부로 구성되어 있으며 위젯의 라이프사이클 관리 및 상태 변경을 담당합니다. 위젯 트리에는 개발자가 작성한 코드에 대한 구조 정보만 있었다면, 엘리먼트 트리에는 위젯트리를 기반으로 생성된 위젯의 일부 조각이 존재하게 됩니다.

위젯 트리가 자동차 설계도면에 비유되었다면, 엘리먼트 트리의 Element는 자동차 부품과 해당 부품을 관리하는 엔지니어라고 할 수 있습니다. 자동차 엔지니어는 설계 도면에 따라 적재적소에 필요한 부품을 배치 및 관리하는 것처럼, 엘러먼트 트리는 위젯이 최종적으로 그려지기 위한 UI의 일부 조각인 Element를 생성하고 필요에 따라 렌더 트리에 변경 사항을 전달합니다. 그럼, Element의 특성을 몇 가지 좀 살펴볼까요.

위젯은 immutable element는 mutable

모든 Flutter 위젯은 immutable하기 때문에 런타임 중에 내용을 수정할 수 없습니다. 이는 자동차가 갑자기 오토바이로 변할 수 없는 것과 비슷합니다. 그러나 엘리먼트는 mutable하기 때문에 필요에 따라 위젯을 변경할 수 있습니다. 즉, 엘리먼트는 삭제되고 새로운 엘리먼트로 대체될 수 있습니다.

BuildContext의 역할

우리가 StatlessWidget 또는 StateFullWidget에서 build 메소드를 실행할 때마다 항상 인자로 넘겨주었던 BuildContext는 Element 객체의 직접적인 조작을 제어 및 접근을 필요할 때 사용됩니다. 또한 위젯트리로부터 생성된 Element가 어떤 노드에 위치하는지 알려주는 역할을 담당합니다. 마치 엔지니어(BuildContext)가 설계 도면(Widget Tree)을 보고 필요한 부품(Element)이 어디에 있는지 파악하고 적절하게 배치하는것 처럼요.

showDialog<void>(  
  context: context,  
  builder: (BuildContext context) {  
    return AlertDialog(...);  
  },  
);

마찬가지로, showDialog 같은 메소드를 이용하여 팝업을 노출할 때도 항상 BuildContext를 넘겨주어야 했던 이유는 트리에 구성된 여러 위젯중에 어떤 위젯(화면)에 Dialog를 띄어줄지 알아야 하기 때문입니다.

핵심 포인트

  • 엘리먼트 트리는 위젯의 라이프 사이클을 관리하고 필요에 따라 변경 사항을 렌더 트리에 전달함.
  • BuildContext는 현재 화면에 보여지는 위젯의 위치를 파악하는 데 사용되며, Element를 조작하거나 접근할 때 중요한 역할을 함.
  • BuildContext도 하나의 Element로 간주됨.

Rendering Tree - 자동차 조립, 조립된 자동차

필요한 Element가 생성되었다면, 마지막으로 위젯은 Render Tree를 만듭니다. 위젯의 createRenderObject 메소드를 통해 실제 렌더링을 처리하는 데 사용되며, 이 과정에서는 위젯의 크기와 레이아웃 정보를 관리하는 객체인 RenderObject가 생성됩니다. 이때 Element Tree에서 생성된 RenderObjectElement가 직접적으로 관여하게 됩니다.

렌더링 트리는 자동차 부품을 사용하여 자동차를 조립하는 단계라고 비유할 수 있습니다. Element Tree에서 구성한 자동차 부품을 이용하여 자동차를 완성하는 것이죠.

렌더링 트리에서는 layout , paint 이렇게 크게 2가지 메소드를 통해 실질적으로 우리의 눈앞에 보이는 위젯을 그립니다. layout 단계에서는 부모 노드가 자식 노드로부터 제약 조건을 전달하고, 최하위 노드에서는 최종적으로 결정된 크기 정보를 다시 위로 전달하여 위젯이 어떤 위치에 어떤 크기로 그려질지를 결정합니다. 그 후에는 paint 작업이 이루어져서 GPU 스레드에 작업을 전달하여 최종적으로 위젯이 완성됩니다.

핵심 포인트


만약 3가지의 위젯트리로 구성되어있지 않았다면?

이제 위젯 트리의 구조에 대해 어느 정도 이해하셨다면, 왜 위젯이 3가지의 위젯 트리로 구성되는지에 대한 이유를 명확하게 이해하실 수 있을 겁니다. 만약 여러분이 자동차에 새로운 바퀴를 교체한다면, 자동차를 처음부터 다시 만들 필요가 없을 것입니다. 기존의 바퀴를 새로운 바퀴로 교체하기만 하면 되겠죠. Flutter에서도 위젯이 비슷한 원리를 가집니다. 화면이 그려진 위젯의 일부가 상태에 따라 변화해야 할 때, 해당 위젯에 해당하는 엘리먼트가 이를 감지하고 렌더 트리로 변경 사항을 전달하여 필요한 부분만 다시 렌더링할 수 있게 합니다.

근데 만약 Flutter의 위젯트리가 하나로만 구성되어 있었다면, 상태에 따라 변경되지 않아도 되는 위젯도 다시 그려지는 비효율이 발생했을 겁니다. 자동차를 바퀴를 바꾼다고 새로운 자동차를 만드는 꼴이죠.

요약하자면, Flutter의 위젯이 3가지의 트리 구조로 구성된 가장 핵심적인 이유는 상태에 따라 화면이 변경되어야 할 때 전체 화면을 다시 렌더링하는 것이 아니라 변경이 필요한 부분만 효율적으로 다시 렌더링하기 위함입니다.


1. 위젯트리를 분리하고 BuildContext로 렌더 객체에 접근하기

자, 이제 자식 위젯에 따라 가변적인 크기를 가지는 위젯의 크기를 도출하는 방법을 단계별로 알아보겠습니다.

앞서 언급한 대로, 렌더링된 위젯의 크기는 렌더 트리에 위치한 RenderObject에 존재하며, 해당 객체에 접근하기 위해서는 BuildContext라는 Element가 필요합니다. 결과적으로 BuildContext만 있으면 위젯의 렌더링된 크기에 접근할 수 있는 공식이 성립합니다.

Size size = context.size!;

그리하여 위 코드처럼 BuildContext로 접근 가능한 위젯의 렌더링 사이즈 확인할 수 있습니다.

다만, 현재 예제 코드의 위젯 트리에서는 한 가지 문제점이 있습니다.

Column 위젯에 해당하는 RenderObject를 통해 위젯의 렌더링된 크기를 얻고 싶지만, RenderObject에 접근 가능한 BuildContext는 그보다 상위에 위치한 CaseInfoScreen에 위치하고 있습니다. 이렇게 된다면 Column의 크기뿐만 아니라 Scaffold의 하위 위젯으로 구성된 AppBar 크기도 함께 측정할 수밖에 없습니다.

이 문제를 해결하기 위해서는 여러 방법이 있겠지만, 가장 단순한 방법은 크기를 측정하고자 하는 위젯을 StatelessWidget 또는 StatefulWidget처럼 별도의 위젯으로 분리하는 것입니다. 이렇게 되면 분리된 위젯의 build(BuildContext context) 메소드를 통해 BuildContext가 직접 접근 가능한 새로운 하위 위젯 트리가 생성되고 Column의 렌더링된 크기에 접근할 수 있습니다. 이를 흔히 "BuildContext를 분리한다" 라고 표현하기도 합니다.

class ContentView extends StatefulWidget {
  const ContentView({super.key});

  
  State<ContentView> createState() => _ContentViewState();
}

class _ContentViewState extends State<ContentView> {
  double renderedHeight; // <-- [ContentView] 위젯의 렌더링된 높이

  
  void initState() {
	  super.initState();
	  /// [BuildContext]를 통해 위젯의 렌더링 크기에 접근하여
	  /// renderedHeight 변수에 할당
      renderedHeight = context.size?.height ?? 0; 
  }

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
	      Text(
           'Height : ${renderedHeight}', // <- 렌더링 높이를 Text 위젯으로 표시
          ),
        ),
        ...
      ],
    );
  }
}
      

이제 위 코드처럼 크기를 측정하고자 하는 위젯을 별도의 StatefulWidget으로 분리하여, 새로운 build 메소드 내부의 BuildContext를 통해 size라는 값을 얻어 우리가 측정하고자 하는 위젯의 렌더링된 높이를 도출할 수 있습니다.


2. 위젯의 렌더링이 완료된 시점을 파악하여 크기 구하기

하지만 위 코드를 실행시키면 아래와 같은 런타임 오류가 발생합니다.

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Builder(dirty):
Cannot get size during build.

왜 이런 오류가 발생할까요?

앞서 설명한 대로, 렌더 트리에서는 layout 메소드를 통해 부모 노드는 자식 노드로부터 제약조건을 전달하고, 최하위 노드에서는 최종적으로 결정된 크기 정보를 다시 위로 올려보내 위젯이 어떤 위치에 어떤 크기로 그려질지를 정하는데, 아직 layout 메소드가 실행되지 않은 시점에서 사이즈 값에 접근하려고 해서 발생하는 문제입니다.

이를 해결하기 위해 Flutter에서는 addPostFrameCallback 메소드를 제공합니다. 이 메소드는 위젯이 화면에 그려진 후에 호출되는 콜백을 등록하는 데 사용됩니다. 즉, 렌더 트리의 작업이 완료된 후 실행되는 콜백 메서드입니다.

WidgetsBinding.instance.addPostFrameCallback((_) { 
	setState(() {
         renderedHeight = context.size!.height; 
      });
});

위 코드처럼 addPostFrameCallback 콜백 메소드 내부에 이전과 같이 renderedHeight 변수에 BuildContext를 통해 접근할 수 있는 위젯의 렌더링 높이를 할당해 주면, 위젯이 렌더링된 후에만 context를 통해 렌더링 크기값에 접근하므로 오류 없이 정상적으로 renderedHeight 변수에 값을 할당할 수 있게 됩니다.


3. 위젯의 크기가 동적으로 변할 때 이를 감지하고 변화된 크기 구하기

필요한 요구사항을 대부분 충족했지만, 위젯의 크기가 유저의 인터렉션으로 인해 변할 때 이를 감지하고 변화된 크기를 화면에 보여주는 기능이 하나 남았습니다.

화면의 ExpansionListTile 위젯을 클릭하면 위젯이 확장되어 크기가 변하지만, 현재 코드에서는 위젯의 크기 변화를 감지하고 크기가 값을 업데이트하고 있지 않죠.

이때 위젯의 크기 변화를 감지하고 특정 이벤트를 실행하는데 유용한 NotificationListener라는 위젯을 사용해 볼 수 있습니다. 일반적으로 NotificationListener은 위젯 트리에서 발생하는 알림(크기 변화, 스크롤, 제스쳐 등)을 수신하고 처리하는데 사용됩니다.

NotificationListener(  
  onNotification: (_) { 
    if(renderedHeight != context.size!.height) {
	  setState(() {  
		renderedHeight = context.size!.height;  
		log('height : $renderedHeight');  
	   });  
    }
    return true;  
  },  
  child: Column(...)
  )

기존의 Column 위젯을 NotificationListener 위젯으로 감싼 뒤, onNotification 콜백 내부에서 위젯의 크기 변화를 감지하고 해당 크기를 업데이트하는 로직을 추가합니다. 위 코드에서는 setState 메소드를 사용하여 변경된 크기를 업데이트하고 있습니다.

참고로 NotificationListener는 스크롤 또는 터치 제스쳐등의 이벤트를 감지하고 onNotification 콜백이 실행되어 위젯의 크기가 변하지 않았지만 동일한 크기 값을 불필요하게 업데이트할 수 있기 때문에 if(renderedHeight != context.size!.height) 이라는 조건 안에서만 업데이트 로직을 실행하도록 조건문을 붙였습니다.

여기서 onNotification 콜백 메소드 안에서 위젯의 높이 크기를 출력하는 로그를 통해 한 가지 문제점을 확인할 수 있습니다.

[log] height : 367.0
[log] height : 367.0
[log] height : 369.3854225873947
[log] height : 375.8518112897873
[log] height : 385.81551444530487
[log] height : 435.25
[log] height : 413.13516367971897
[log] height : 430.28125
[log] height : 448.9247215986252
[log] height : 469.3435592651367
[log] height : 491.38857555389404
[log] height : 551.9744523763657
[log] height : 540.0271100997925
[log] height : 567.0

위젯의 크기가 변할 때마다 onNotification 콜백이 연속적으로 실행되어 불필요하게 setState 메소드가 여러 번 호출되고 있네요. 이러한 부분은 성능 저하로 이어질 수 있기 때문에 수정이 필요합니다.

이 문제를 해결하기 위해 디바운서(debouncer) 로직을 추가할 수 있습니다. 디바운서는 연속적인 호출을 일정 시간 동안 지연시키고, 마지막 호출 이후에만 작업을 실행할 수 있게 도와줍니다.

/// Deboucner 모듈
class Debouncer {  
  final Duration delay;  
  Timer? _timer;  
  
  Debouncer(this.delay);  
  
  void run(VoidCallback action) {  
    _timer?.cancel();  
    _timer = Timer(delay, action);  
  }  
}

/// ContentView 위젯
double? renderedHeight;  
final Debouncer debouncer = Debouncer(const Duration(milliseconds: 50));

Widget build(BuildContext context) {  
	return NotificationListener(  
	onNotification: (_) {  
	    debouncer.run(() {  // <-- 디바운서 콜백 적용 
		    if(renderedHeight != context.size!.height) {
			  setState(() {  
				renderedHeight = context.size!.height;  
				log('height : $renderedHeight');  
		   	});  
		   }
	      });  
	      return true;  
	    },  
	    child: Column(...)
	... 
   }

위 코드에서는 디바운서 클래스를 선언하고, onNotification 콜백 내부에서 디바운서를 사용하여 setState 메소드를 호출하여 BuildContext로부터 접근된 size값을 업데이트하고 있습니다. 이를 통해 최종적으로 ExpansionTile 위젯의 크기가 확장된 후에만 업데이트 메소드가 호출되어 성능을 최적화할 수 있습니다.


4. 사용하기 편하게 모듈화 하기

가변적인 위젯의 렌더링 크기를 구하는 기능은 다른 화면 또는 여러 프로젝트에서 적용될 수 있기 때문에 사용하기 쉽게 모듈화 해 보는 것도 좋은 방법입니다.

그래서 저는 MeasureSizeBuilder라는 커스텀 위젯을 만들었습니다. 이 위젯은 앞서 다루었던 모든 로직을 포함하며, builder 속성에 크기를 측정하고자 하는 위젯을 반환하여 해당 위젯의 렌더링 크기를 builder 내부의 size 속성으로 접근할 수 있도록 설계되었습니다.

이제 완성된 예제의 코드를 확인해볼까요?

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:measure_size_builder/measure_size_builder.dart';
import 'package:measure_size_implementation/src/cast_model.dart';
import 'package:measure_size_implementation/src/style/app_color.dart';
import 'package:measure_size_implementation/src/style/app_text_style.dart';

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: MeasureSizeBuilder( <-- // MeasureSizeBuilder 적용!
            builder: (context, size) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Height : ${size.height}',
                    style: PretendardTextStyle.bold(
                      size: 24,
                      height: 37,
                      letterSpacing: -0.2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  ListView.separated(
                    physics: const NeverScrollableScrollPhysics(),
                    padding: EdgeInsets.zero,
                    shrinkWrap: true,
                    itemCount: CastModel.castList.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 8),
                    itemBuilder: (context, index) {
                      final item = CastModel.castList[index];
                      return ExpansionTile(
                        tilePadding: EdgeInsets.zero,
                        title: Row(
                          children: [
                            ClipRRect(
                              borderRadius: BorderRadius.circular(56 / 2),
                              child: CachedNetworkImage(
                                height: 56,
                                width: 56,
                                imageUrl: item.imgUrl,
                                fit: BoxFit.cover,
                              ),
                            ),
                            const SizedBox(width: 10),
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  item.name,
                                  style: AppTextStyle.title1,
                                ),
                                Text(
                                  item.role,
                                  style: AppTextStyle.body3.copyWith(
                                    color: AppColor.gray02,
                                  ),
                                )
                              ],
                            ),
                          ],
                        ),
                        children: <Widget>[
                          Text(
                            item.description,
                            style: AppTextStyle.body3,
                          ),
                        ],
                      );
                    },
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

그리고 마침내 드디어 앞서 우리가 목표로 했던 요구사항을 모두 충족했습니다🎉

예제에 적용한 measure_size_buidler라는 패키지를 배포하였으니 해당 기능이 필요하거나 모듈화된 코드가 궁금하신 분들은 아래 링크를 참고해 주세요!
👉 measure_size_builder 1.0.2


마무리하면서

이번 포스팅에서는 가변적인 크기를 가지는 위젯의 렌더링 크기를 구하는 방법과 함께 위젯의 렌더링 원리에 대해 살펴보았습니다. 사실 Flutter의 렌더링 원리에 대해 깊게 들어갈수록 굉장히 내용이 어려워지고 복잡해지기 때문에 본 포스팅에서는 간단한 개념만 다루었는데요. 혹시 자세한 Flutter 렌더링 원리에 대해 관심이 있으시면 아래 포스팅을 참고해 보시면 좋을 것 같습니다. 정말 자세히 잘 정리되어 있습니다.
👉 flutteris

혹시 영어를 읽는게 부담스러우시면, Broccolism님이 Velog에 작성한 공식 문서 읽기 프로젝트 시리즈를 읽어보시길 강력히 추천드립니다. 21년도에 작성된 글이지만, Flutter 렌더링을 주제로 이보다 잘 작성된 한국어 포스팅은 없는 것 같네요.
👉 공식 문서 읽기 프로젝트(flutter)

또한 포스팅에서 다룬 예제의 전체 코드가 궁금하시면 제 깃허브 레포를 참고해주세요.
👉 깃허브 레포

다음에는 조금 더 유익한 주제로 돌아오겠습니다.
읽어주셔서 감사합니다:)

profile
https://medium.com/@ximya

7개의 댓글

comment-user-thumbnail
2024년 6월 3일

좋은 글 잘 읽고 있습니다!

1개의 답글
comment-user-thumbnail
2024년 7월 1일

항상 잘 보고있습니다!

1개의 답글
comment-user-thumbnail
2024년 8월 23일

이전 글에서 멘토링을 준비하고 계시다는 글을 봤는데 혹시 지금 참여가 가능할까요..?
flutter 취준생입니다!

1개의 답글