[Flutter/Decoding Flutter] ShrinkWrap vs Slivers

작성 날짜:

최근 업데이트 날짜:

Decoding Flutter 유튜브 영상


ShrinkWrap vs Slivers

‘Vertical viewport was given unbounded height.’ 에러

Flutter를 처음 배우는 사람이 가장 먼저 접하는 것 중 하나가 ListView 위젯이다. ListView 위젯을 통해 간편하게 여러 위젯들을 나열하고 스크롤할 수 있게 만들 수 있기 때문이다.

ListView를 처음 배우고 신나게 개발하다가, 어느 순간에 빨간 화면과 함께 아래와 같은 에러를 마주치게 된다.

Vertical viewport was given unbounded height.

Flutter를 배우는 모든 개발자는 이 에러를 마주친 적이 있을 것 같다. 그리고 이 에러를 어떻게 해결할 수 있는지 모두 잘 알고 있을 것이다. 일반적으로 이 에러는 ListView안에 ListView를 사용할 때 발생하며, 하위 ListViewshrinkWrap을 true로 설정하면 해결된다.

하지만 여기에는 여전히 문제가 있다고 한다. 지금까지 위와 같은 상황을 마주하면 생각 없이 기계적으로 shrinkWrap을 true로 설정하고 넘어갔고, 아무 문제도 없을 것이라고 생각했기 때문에 놀랐다.

ShrinkWrap의 문제점

shrinkWrap을 사용하는 것의 문제는 비용이 많이 든다는 점이라고 한다. 왜 그럴까? 이를 알기 위해서는 우선 ListView의 작동 방식에 대해 알아야한다.

ListView에는 사용자에게 보이는 높이와 내부 높이가 따로 존재한다. 사용자에게 보이는 높이는 상위에서 따로 정해줘야하고, 내부 높이는 자식 위젯들에 따라 무한히 늘어난다.

그럼 ListView(상위) 안에 ListView(하위)가 있는 경우를 생각해보자. 이 경우에 상위 ListView는 내부 높이가 자식 위젯들에 따라 늘어나야하는데, 반대로 하위 ListView는 사용자에게 보이는 높이를 상위에서 받아야 한다. 따라서 서로가 상대방의 높이를 알아야 자신의 높이를 정할 수 있는 교착 상태가 된다. 결국 아무 높이를 정할 수 없고 에러가 발생하게 된다.

그래서 이를 해결하기 위해 하위 ListView의 사용자에게 보이는 높이를 자신의 내부 높이와 똑같이 늘린다. 이렇게 되면 한쪽의 높이가 정해졌기 때문에 교착 상태가 풀리게 된다. 이것이 바로 하위 ListViewshrinkWrap을 true로 설정하면 일어나는 작업이다.

그렇다면 이게 왜 문제가 되는 것일까?

만약 shrinkWrap을 true로 설정한 하위 ListView가 굉장히 길고 애니메이션 효과를 가지고 있다고 생각해보자. 해당 ListView의 높이를 자신의 내부 높이와 동일한 길이로 늘려야한다. 그렇기 때문에 길고 복잡한 자식 위젯들을 한번에 전부 렌더링해야한다. 이렇게 되면 프레임 드랍과 애니메이션의 버벅임이 발생하게 된다.

해결 방법

아래와 같은 코드가 있다고 생각해보자.

final outerListChildren = <ListView>[
  ListView(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    children: <Widget>[...],
  ),
  ...
];
return ListView.builder(
  itemCount: outerListChildren.length,
  itemBuilder: (context, index) {
    return outerListChildren[index];
  },
);

예시에선 상위 ListView의 자식 위젯들을 전부 ListView로 가정했다. 내부 ListView들은 당연히 에러를 피하기 위해 전부 shrinkWrap을 true로 설정해놨다. 그렇기 때문에 내부 ListView들이 복잡할수록 문제가 더 생길 것이다.

다행히 해결 방법은 간단하다.

return CustomScrollView( // ListView 대신 CustomScrollView 사용.
  children: outerListChildren,
)

우선, 상위 ListViewCustomScrollView로 바꿔줘야한다.

final outerListChildren = <SliverList>[]; // ListView 대신 SliverList 사용.

return CustomScrollView(
  slivers: outerListChildren,
)

다음으로, 하위 ListViewSliverList로 변경한다.

final outerListChildren = <SliverList>[
  SliverList(
    delegate: SliverChildBuilderDelegate( // 기존 하위 `ListView`의 자식 위젯들을 넣는다.
      (context, index) => _myWidgets[index],
      childCount: _myWidgets.length,
    )
  )
];

return CustomScrollView(
  slivers: outerListChildren,
)

기존 하위 ListView의 자식 위젯들을 SliverListdelegate에 넣어준다. 만약 기존 하위 ListView에서 .builder 생성자를 사용했다면, delegateSliverChildBuilderDelegate를 사용하면 된다. 두가지가 매우 비슷한 형태이기 때문이다.

벌써 끝났다. shrinkWrap을 사용했을 때와 다르게, 이제 각 하위 ListView들의 내부가 복잡하더라도 효율적으로 빌드된다.

댓글남기기