활동량 페이지를 만들면서 위아래로 둥둥 움직이고, 조건에 따라 보이고 / 안보이는 챗버블 위젯을 만들게 되었습니다.
저는 조건이 true이면 버블을 빌드하고, false면 SizedBox.shrink()를 빌드하는 코드를 작성했었습니다.
그런데 pr로 '뎁스를 사라지게 하는것보다 Visibility, Offstage를 사용하고 상태관리를 사용하는 것이 좋다'는 피드백을 받게 되어 둘중 뭘 사용하는게 더 좋을지 비교하며 찾아 보게 되었는데요,
이것을 정리해서 공유해 보려고 합니다!
앱을 만들면서 조건에 따라 보여주거나 보여주지 않는 UI를 만들일이 많이 있습니다.
한번 결정되어서 변동이 없는경우에는 if statement 을 사용해서 위젯의 빌드 여부를 결정하는 방식을 사용해도 뎁스가 변동될 일이 없어 괜찮을 것입니다. 하지만 if statement를 사용해서 조건에 따라 위젯을 보이고 / 안보이게 만든다면 위젯의 계층구조가 수정되기 때문에, 트리의 해당 부분에 있는 모든 위젯이 다시 작성될 것입니다.
특히 위젯 트리가 복잡하거나 build 메서드에 리소스 집약적인 작업이 포함된 경우 성능에 영향을 미칠 수 있으니 주의해야 겠지요.
따라서 상태에 따라 보이고 / 안보이고가 변할때는 트리의 뎁스를 유지할 수 있도록 도와주는 위젯들을 사용하는것이 좋은데요,
이 경우에 사용해볼 수 있는것은 Opacity, Visibility, Offstage가 대표적입니다. 애니메이션을 사용한다면 AnimatedSwitcher, AnimatedCrossFade도 사용해 볼 수 있겠지만 저는 위의 세 위젯만 다루도록 하겠습니다.
Opacity
opacity로 0.0~1.0사이값을 줘서 위젯을 투명하게 만들 수 있습니다.
Opacity를 사용하면 화면에 위젯이 보여도 / 안보여도 똑같이 자리를 차지하고, 애니메이션도 유지됩니다.
하지만 intermediate buffer도 사용하게 되기 때문에 성능이 좋지는 않다고 합니다.
그런데 intermediate buffer가 뭘까요? 찾아보다가 자료가 없어 바드에게 물어보니 intermediate buffer는 위젯의 자식위젯을 그린 후에 다시 해당 위젯의 불투명도에 따라 다시 그리는데에 사용된다고 합니다. intermediate buffer를 사용하기 때문에 전체화면을 그릴필요 없이 투명한 위젯을 효율적으로 그리게 해준다고 합니다. 하지만 intermediate buffer가 너무 커지고 많아지면 성능이 저하될 수 있다고 하네요.
플러터가 intermediate buffer를 사용해서 위젯을 그리는 과정은 아래와 같습니다.
Container(
color: Colors.red,
child: Opacity(
child: Text('Hello, world!'),
opacity: 0.5,
),
)
- Flutter는 Opacity 위젯을 그릴 때 먼저 intermediate buffer를 생성합니다.
- Flutter는 Opacity 위젯의 자식 위젯인 Text 위젯을 intermediate buffer에 그립니다.
- Flutter는 intermediate buffer의 불투명도인 0.5를 적용하여 위젯을 다시 그립니다.
- Flutter는 최종 결과를 화면에 표시합니다.
intermediate buffer에 대해 설명할때는 덕분에 성능이 좋아졌다고 하면서, Opacity에대해 설명할때는 intermediate buffer를 사용하기 때문에 성능이 좋지 않다는 모순된 내용들을 찾을 수 있었는데요,
종합해보면 투명도를 구현하기에는 intermediate buffer가 효과적이지만, 보여지고 / 보이지 않는 위젯을 구현하는데에는 다른 위젯의 성능이 더 좋기때문에 ‘성능이 좋지 않다’고 평가되는 것 같습니다.
Visibility
Visibility 위젯은 자식 요소가 화면에 표시되어야 하는지 여부를 제어하는 데 사용할 수 있는 유용한 위젯 입니다.
Visibility 위젯을 사용하면 visible이 false일에 child위젯이 그려질 자리를 비워놓지 않고, 다른 위젯이 그 자리 다른 위젯이 차지하도록 하기 때문에, 위젯트리의 구조를 변경하지 않을 수 있습니다.
replacement를 따로 설정하지않으면 그자리를 디폴트로 SizedBox.shrink()가 차지하게 되는데, 이 방법을 통해 위젯이 자리를 차지하지는 않지만 트리에서는 child가 존재하도록 만들 수 있습니다.
Offstage
Offstage를 사용하면 위젯을 위젯트리에서 제거하지않고 숨길 수 있습니다.
offstage:true 로 위젯을 숨겼을때에 위젯을 laid out 하지만(위젯의 크기와 위치를 계산한다) 화면에 paint 하지 않습니다. 즉 화면에서 공간을 차지하지 않습니다.
숨긴 위젯을 다시 보이게 하더라도 숨겨진 위젯을 리빌드 하지 않는다는 점에서 성능이 좋다고 할 수 있습니다.
이때문에 생기는 단점은 애니메이션이 있을경우에는 화면에 보여지지 않아도 애니메이션이 실행된다는 것입니다. 따라서 cpu와 배터리를 사용하게 되고 이런점에서는 성능이 좋지 않을 수 있습니다.
애니메이션이 실행되는 위젯이라면 비용을 고려해서 어떤방식이 좋을지 생각해봐야 할 것입니다.
추가로 OffStage는 화면에 paint되지는 않지만 laid out되기 때문에, 즉 위젯의 크기를 계산해두기 때문에 화면에 노출하지 않으면서 위젯의 크기를 구하고 싶은 경우에도 사용한다고 합니다.
위젯이 실제로 어떻게 그려지는지 알고싶어서 간단히 테스트 해봤는데요,
각 위젯으로 child를 숨겼을때 Widget Tree가 어떻게 그려지는지 저번주에 배운 Widget Inspector를 사용해서 살펴보도록 하겠습니다
Opacity
opacity:0 일때도 자식위젯이 위젯트리에 유지되는 것을 볼 수 있습니다. 컨테이너의 패딩까지 나오는 것을 보면 크기까지 다 그려지고 있습니다.
앞서 설명 했듯 보이지는 않더라도 자식 위젯의 크기 그대로를 차지합니다.
Offstage
offstage:true일때 renderObject를 보면 Size(0.0 , 0.0)인것을 볼 수 있습니다.
laid out되어 크기는 계산 되었겠지만 화면에 paint는 되지 않고 사이즈가 0입니다
하지만 애니메이션을 추가해두었기때문에 보이지는 않더라도 애니메이션이 실행될 것입니다.
Visibility
visibility:false일때 child위젯이 laid out 되지도 paint되지도 않습니다. 대신 그 자리에 SizedBox.shrink가 들어가있네요.
추가로 if statement
if statement를 사용하면 위젯트리에 해당 계층이 포함되지 않는다는 것도 확인해 볼 수 있었습니다.
그러면 어떤 위젯을 사용해서 조건에따른 위젯을 보여줘야 할까요?
저는 앞에서 말한 활동량 챗버블 위젯을 만들때 Visibility위젯을 사용해서 숨겨지도록 했습니다. 이유는 Offstage위젯을 사용하면 화면에 보이지 않더라도 애니메이션이 계속 실행되기 때문입니다.
하지만 애니메이션이 있는 위젯이 아니라면 위젯을 숨겼다 다시 그릴때의 비용이 적은 Offstage를 사용했을 것 같습니다.
조사를 하면서 laid out, paint가 정확히 뭔지 와닿지 않아서 렌더링 과정에 대해서도 조금 알아봤는데요,
간략히 잘 정리된 자료가 있어서 공유합니다.
이미지를 보면서 OffStage위젯을 생각해보면 ‘레이아웃’까지는 되었지만 ‘그리기’가 Size(0.0)으로 된거구나 하고 알수 있었습니다.
좀더 본격적인 Flutter Rendering Pipeline 이미지도 찾을 수 있었는데
우리가 이전에 StatefulWidget과 State 에서 공부한 Widget, Element, RenderObject를 다시 생각해보면 좋을 것 같습니다.
간단하게 표현하면 Flutter Framework는 Tree 구조의 Element들로 이루어져 있고, 각 Element들은 대응되는 Widget과 Render Object, 그리고 필요하다면 State를 갖고 있습니다.
다음에는 렌더링 과정을 조금 더 자세히 살펴봐야겠습니다.
봐주셔서 감사합니다!
이미지 출처
'Weekly Flutter' 카테고리의 다른 글
flutter 렌더링 과정에 대해서 알아보자 (0) | 2023.07.13 |
---|---|
Generic을 파다가 variance를 파다가 리스코프의 법칙을 파다가.. (0) | 2023.07.06 |
backgroundservice를 사용해보자. (0) | 2023.06.29 |
[Flog Favorite] go_router 패키지 (0) | 2023.03.22 |
다트의 인스턴스 복사 (0) | 2023.03.15 |