Flutter Design Patterns and Best Practices Study #3

김민진·2025년 3월 21일
0

개발공부

목록 보기
11/11
  • 여기서의 ListView는 ListView.builder를 뜻한다.

우리는 Flutter 에서 ListView를 굉장히 흔하게 사용한다.

하지만 ListView의 아이템들이 불필요하게 paint 된다고 생각해본적이 있는가?

ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ItemWidget(item: items[index]);
      },
    );

제일 흔하게 사용하는 형태이다.

만약 ItemWidget에서 setState를 하게 된다면..?

여기서 2가지 케이스로 나뉜다.

  1. ItemWidget이 별도의 class이다.

만약 별도의class로 분할되어져 있다면 위젯트리가 별도로 존재하게 되므로

하나의 아이템만 rebuild 된다.

  1. ItemWidget이 같은 class에서 함수로 빠져있다.

이 경우에는 이야기가 달라진다. setState는 build 메서드를 호출하게 되므로

전체의 ListView가 다시 그려지게 된다.

물론 여기서 ItemWidget을 class로 나눈다면 큰 걱정? 까지는 할 필요가 없다.
rebuild 되는 부분을 나눈다는 이야기이고 이 것은 성능에 큰 영향을 끼치지는 않게 된다.

하지만 같은 클래스 내부에 존재한다면 setState를 통한 상태변경을 최악의 선택지가 될 수 있다.

간단한 예시로 ListView만 말했지만 build 내부에 있는 위젯트리 모두가 rebuild 되버리니까..

일단 우리는 ListView만 신경쓰자

ListView에 속성이 뭐가 있는지 본적이 있는가?

 ListView.builder({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    this.itemExtent,
    this.itemExtentBuilder,
    this.prototypeItem,
    required NullableIndexedWidgetBuilder itemBuilder,
    ChildIndexGetter? findChildIndexCallback,
    int? itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    int? semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
    super.hitTestBehavior,
  })

여기서 모든 속성을 보지는 않겠지만 지금 눈여겨볼 속성은 단 하나이다.

addRepaintBoundaries

이 속성이 뜻하는 바를 본적이 있는가?

이 속성은 ListView가 그리게 될 하위 위젯들을 사실상 RepaintBoundary 로 감싸게 된다.

setState가 발생한다고 해도 모든 ListView의 위젯들을 다시 그린다 라기 보다

내부의 ListView 아이템의 변경되는 부분만 다시 그리게 된다. 라고 간단하게 봐주면 될듯 하다.

타입이 같고(ItemWidget), key가 같으면 Element를 재사용합니다.
하지만 속성(item)이 다르면, Element의 속성을 업데이트하고 해당 위젯의 build 메서드를 다시 호출합니다.

다시 그리게 된다라는 의미는 위의 내용으로 요약할 수 있다.

addRepaintBoundaries 이 옵션은 다행스럽게도 기본값이 true이다.

우리는 큰 걱정없이 ListView가 있는 상태에서도 setState를 하게되면 Flutter 내부에서 매우 효과적으로 처리하고 있어서 큰 신경을 쓰지 않아도 된다.

이제 좀더 나아가보자

나는 그랬지만.. ListView를쓸때 shrinkWrap 을 true로 사용했다.

무한한 높이를 가진 Listview를 그릴때 저 옵션 하나면 에러가 발생하지 않아서

단순히 그냥 하나만 해주면 뚝딱이네~ 라고 생각했다.

하지만 책에는 이런 내용이 나온다.


First, if you want to build a list of homogeneous items, the most efficient way to do so is by
using the ListView.builder constructor. The beauty of this approach is that at any given
time, by using the itemBuilder callback that you’ve specified, the ListView will render
only those items that can actually be seen on the screen (and a tiny bit more, as determined by
the cacheExtent). This means that if you have 1,000 items in your data list, you don’t need
to worry about all 1,000 of them being rendered on the screen at once – unless you have set
the shrinkWrap property to true.
This leads us to the second tip: the shrinkWrap property (available for various scroll views)
forces the scroll view to calculate the layout of all its children, defeating the purpose of lazy
loading. It’s often used as a quick fix for overflow errors, but there are usually better ways to
address those errors without compromising performance. We’ll cover how to avoid overflow
errors while maintaining performance in the next chapter.

shrinkWrap 의 역할이 뭔지 이 내용을 보고 다시한번 찾아보게 되었다.

그냥 무한한 높이를 가진 ListView를 사용할때 적용해주는 옵션인게 아닌가??

ListView는 기본적으로 Lazy Loading 방식이다.

화면에 보이는 아이템만 빌드 및 렌더링이 발생하지만

해당 옵션을 True로 변경하는 순간 모든 자식 위젯의 레이아웃을 계산해야 한다.

높이가 정해지지 않았으므로 보이지 않는 아이템까지 모두 크기를 계산한다는 의미이다.

아이템의 갯수가 몇개 되지 않는다고 하면 성능에 무리는 없겠지만..

무한 스크롤을 지원하는 경우가 된다면 ..? 페이지 네이션으로 적절하게 분배를 하겠지만.. 서버와의 연동시에 어쩔수없이 너무 많은 아이템을 가져온다면?

그러게 된다면 스크롤시 성능이 제한되고 60fps를 제공하지 못할 가능성이 크게 된다.

각 아이템의 가지는 높이가 50이라고 가정하고 아이템의 갯수는 1000개라고 가정하자

그러면 ListView가 build 되깅 위해서는 50000의 높이를 미리 계산해서 가지고 있어야 한다.

그렇게 되면 초기로딩도 느려지고 스크롤시 렉이 발생할수 있다.

이렇게 되면 우리는 2가지 대안을 가질 수 있다.

1.ListView를 SizedBox 혹은 Container로 감싸서 정적으로 높이를 지정해주기.

그렇게 된다면 shrinkWrap 옵션을 사용하지 않아도 된다.

  1. Expanded 를 사용하기
    이 또한 화면에 보이는 아이템만 랜더링 하기 때문에 ListView의 LazyLoading 을 유효하게 사용할 수 있다.

결국 shrinkWrap은 효과적으로 우리에게 에러를 제거해주지만 성능상으로 보자면 최악의 옵션이 될 수 있다.

profile
dart,c#,java 개발자! 잡다하게 해서 문제될게 없다!

0개의 댓글