Flutter, ListView.builder 렌덩링 최적화

Ximya·2023년 4월 6일
2

Plotz 개발일지

목록 보기
4/12
post-thumbnail

해당 포스팅은 유튜브 영화&드라마 리뷰 영상 큐레이션 플랫폼 Plotz를 개발하면서 도입된 기술 및 방법론에 대한 내용을 다루고 있습니다.
다운로드 링크 : 앱스토어 / 플레이스토어

Flutter에서 대용량 데이터를 처리하고 화면에 보여주기 위해 ListView.builder를 주로 사용합니다.
ListView.builder는 데이터를 미리 모두 로드하지 않고, 필요한 부분만 동적으로 생성하므로 데이터를 효율적으로 처리할 수 있는 거죠.

예를 들어, 100개의 리스트 아이템이 있을 때, 화면에 보이는 리스트 아이템은 10개일 수 있습니다. 이 경우, ListView.builder는 보이는 10개의 리스트 아이템만 렌더링하고, 스크롤 되는 방향으로 보이지 않게 되는 위젯은 제거합니다. 이를 통해 화면에 보이지 않는 리스트 아이템을 생성하지 않으므로, 메모리사용량과 렌더링 시간을 줄일 수 있습니다.

불필요한 리렌더링

이렇게 ListView.builder를 사용함으로 메모리를 효율적으로 사용하고 렌더링 퍼포먼스를 개선할 수 있지만 1가지 문제점이 있습니다
ListView.builder는 스크롤이 되는 방향으로 보이지 않게 되는 위젯을 제거하기 때문에 이미 한번 렌더링이 된 위젯들을 다시 렌더링 해버립니다.

아래 스크린 녹화 영상을 보면서 자세히 이야기해보죠.

순삭 콘텐츠 상세 스크린의 정보 탭에는 콘텐츠의 이미지 데이터들을 ListView.builder 형태로 보여주고 있습니다. 이때 오른쪽으로 스크롤 하여 더 많은 이미지를 확인하고 다시 첫 번째 이미지 위젯을 돌아간다고 했을 때, 이미 렌더링인 된 이미지 위젯이 한 번 더 렌더링 된다는 걸 Shimmer(스켈레톤)효과로 확인할 수 있습니다.

리스트의 아이템들이 뷰포트에서 사라졌다가 다시 나타날 때 상태를 유지하고 싶으면 어떻게 해야 할까요?

AutomaticKeepAliveClientMixin 적용

Flutter에서 제공하는 AutomaticKeepAliveClientMixin을 통해 ListView.builder의 아이템 위젯들이 리렌더링이 되는 것을 방지할 수 있습니다.
이 mixin은 위젯 트리에서 해당 위젯이 일시적으로 사라져도 여전히 관리하고 싶은 상태를 유지할 수 있도록 도와줍니다.

AutomaticKeepAliveClientMixin을 적용하는 방법은 간단합니다. 먼저, StatefulWidget 클래스를 상속하고, AutomaticKeepAliveClientMixin을 믹스인합니다. 그리고, wantKeepAlive 속성을 true로 설정하여 해당 위젯을 보존하도록 지정합니다. 이렇게 하면, 해당 위젯이 화면에서 사라지더라도 자동으로 보존되고, 이전 상태를 유지할 수 있습니다.

예를 들어, 다음과 같이 AutomaticKeepAliveClientMixin을 적용할 수 있습니다.

class MyWidget extends StatefulWidget {
  
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with AutomaticKeepAliveClientMixin {
  
  bool get wantKeepAlive => true;

  
  Widget build(BuildContext context) {
    super.build(context); 
    return Container(
      // ...
    );
  }
}

모듈화 (KeepAliveView)

이때 이 AutomaticKeepAliveClientMixin mixin이 적용된 StatefulWidget을 ListView.builder부모위젯으로 감싸는게 아니라 itembuilder아이템 위젯에 감싸야 하는 것에 유의해야 합니다.

일단 코드를 간결하기 위해서 해당 StatefulWidget을 모듈화하겠습니다.

class KeepAliveView extends StatefulWidget {
  final Widget child;

  const KeepAliveView({Key? key, required this.child}) : super(key: key);

  
  State<KeepAliveView> createState() => _KeepAliveViewState();
}

class _KeepAliveViewState extends State<KeepAliveView>
    with AutomaticKeepAliveClientMixin {
  
  bool get wantKeepAlive => true;

  
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }
}

KeepAlieView라는 AutomaticKeepAliveClientMixin이 적용된 StatefulWidget을 만들었고, ListView.builder의 아이템을 Argument로 받기 위해 child라는 위젯 타입의 프로퍼티를 설정하였습니다.

ListView.builder(
              padding: const EdgeInsets.only(left: 16),
              scrollDirection: Axis.horizontal,
              shrinkWrap: true,
              itemCount: vm.contentImgList?.length ?? 0,
              itemBuilder: (context, index) {
                final imgItem = vm.contentImgList![index];
                return KeepAliveView(   //<-- KeepAliveView
                  child: CachedNetworkImage(
                    fit: BoxFit.contain,
                    imageUrl: imgItem.prefixTmdbImgPath,
                    height: 100,
                    width: SizeConfig.to.screenWidth - 32,
                    imageBuilder: (context, imageProvider) => Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(6),
                        image: DecorationImage(
                          image: imageProvider,
                          fit: BoxFit.fitWidth,
                        ),
                      ),
                    ),
                    placeholder: (context, url) => Shimmer(
                      child: Container(
                        color: AppColor.black,
                      ),
                    ),
                    errorWidget: (context, url, error) =>
                        const Center(child: Icon(Icons.error)),
                  ),
                );
              },
            )

이제 좀 더 편하게 ListView.builder의 아이템 부분에 AutomaticKeepAliveClientMixin이 믹스인된 StatefulWidget을 감쌀 수 있습니다.


렌더링 측정 결과

그럼 이제 리스트의 아이템들이 뷰포트에서 사라졌다가 다시 나타날 때 리렌더링을 하지 않는지, Flutter Dev Tools를 통해 확인해 봅시다.

측정 방법
ListView.builder 스크롤 방향을 10개의 아이템 위젯을 확인하고, 다시 첫 번째 위젯으로 돌아간다고 했을 때의 경우로 렌더링 횟수를 측정.
이때 KeepAliveView의 자식 위젯인 CachedNetworkImage 위젯을 기준으로 함.

KeepAliveView를 적용하지 않았을 때

KeeapAliveView를 적용하지 않으면 총 ListView.builder의 아이템 위젯이 총 21번 정도 렌더링이 됩니다. 다시 첫 번째 위젯으로 돌아가기 위해 반대로 스크롤을 했을 때 이전에 렌더링이된 위젯도 한번 더 렌더링 된 것이죠.

KeepAliveView 적용

반대로 KeepAliveView를 ListView.builder 아이템을 감싸면 총 13번 정도 렌더링이 됩니다. 기존에 렌더링이 된 위젯의 상태를 AutomaticKeepAliveClientMixin를 통해 유지하기 문에 다시 리렌더링이 일어나지 않습니다.


KeepAliveView을 적용하는게 무조건적으로 좋을까?

앞서 소개해 드린 방식으로 불필요한 위젯 렌더링을 방지하여 앱의 성능을 향상하는 데 도움을 줍니다.
하지만 항상 앱의 성능을 향상시킨다고 말하긴 힘듭니다.

왜냐하면 ListView.builder에서 이미 렌더링이 된 위젯의 상태를 보존하기 위해 메모리를 항상 점유하고 있기 때문입니다. 만약 리스트 아이템이 한번 뷰포트에 노출되고 다시 돌아갈 일이 없는 ListView.builder라고하면, 오히려 불필요하게 렌더링이된 위젯의 상태를 유지하는 거겠죠.

따라서, AutomaticKeepAliveClientMixin을 적용할 때는, 언제 어떻게 사용해야 하는지를 잘 판단하여야 합니다. 불필요한 위젯 렌더링을 방지하여 성능을 향상하기 위해 사용하는 것이 가장 이상적입니다. 그러나,메모리 사용량과 성능 사이의 균형을 잘 조절하여야 하며, 사용하는 상황에 따라 적절하게 사용해야 합니다.

profile
https://medium.com/@ximya

0개의 댓글