티스토리 뷰

"Flutter는 모든 것이 Widget으로 이루어져 있다"라는 말을 들어 보았을 것이다.

우리 개발자들은 Widget만 있으면 앱의 화면을 만들고 기능을 구현할 수 있다.  

특히 StatelessWidget과 StatefulWidget을 만들 수 있고, 기본으로 제공되는 Widget의 사용법만 숙지한다면 간단한 화면 정도는 쉽게 만들어 낼 수 있다. 하지만 Flutter는 정말 Widget만으로 이루어진 것일까?

Widget의 반복적인 생성

개인적으로 처음 Flutter을 접했을 때 가장 궁금했던 것 중 한 가지가 build함수가 재호출 되어 함수 내부에서 Widget들을 다시 생성하는데 어떻게 60 프레임을 보장할 정도로 빠른 화면 구성이 가능할까였다.

코드로만 보면 build함수 내부에서는 항상 새로운 Widget을 만들어 반환하고 있다.  항상 새로운  Widget을 생성하는 것은 분명 비효율적이다. 객체의 생성 비용이 적지 않을 것이며 화면을 구성하기 위해 위치나 크기 등을 계산하는데 우리가 생각하는 것보다 훨씬 많은 연산이 필요할 것이기 때문이다. 

 

StatefulWidget을 예로 들어 보자. State의 setState()를 호출하면 build() 함수가 다시 호출된다.  

Class TestState extends State<TestWidget> {
  late String title;
  
  void initState() {
       this.title = widget.title;
  }

  Widget build() {
     return Column(children: [
       Text(‘Counter : $_counter’),
       TextButton( onPressed: () => updateTitle(DateTime.now().toString()), Text(‘+’));  
     ]);
   }

  void updateTitle(String newTitle) {
    setState((){
         title = newTitle;
    });
  }  
}

 

build함수는 화면 갱신이 필요할 때마다Text, TextButton, Column을 새롭게 생성 후 반환한다.  

 

처음에 위와 같은 코드를 보면서 이런 생각을 한 적이 있다. 

“정말 이렇게 항생 새로운 객체를 생성해도 문제가 없을까?  build함수에서 위젯을 생성하지 않고 미리 생성해둔 Widget을 반환하면 좀 더 속도면에서 빠르고 좋지 않을까?”

그래서 아래와 같이 미리 Widget을 생성한 후 이 위젯을 재사용하도록 만들어 본 적도 있다.

Class TestState extends State<TestWidget> {

  lint _counter = 0;
  late Widget _button;

  void initState() {
      super.initState();
      button = TextButton( onPressed: () => updateTitle(DateTime.now().toString()), Text(‘+’));
  }

  Widget build() {
     return  Column(children: [
         Text(‘Counter : $_counter’),  _button)
      ]);
  }

  void updateTitle(String newTitle) {
    setState((){
         title = newTitle;
     });
  }  
}

 

위 코드를 보면 미리 생성한 button을 계속해서 재사용하고 있다. 

이렇게 만들 경우 변경이 발생하는 Text 위젯은 새로 생성하고 button은 재사용하기 때문에 button 위젯의 생성 비용을 줄일 수 있다. 

하지만 button을 재사용하는 대신 코드 가독성은 떨어지고 추가적으로 해야 하는 일도 많아진다.

과연 이렇게 까지 해야 할까?

 

Button 위젯을 새로 생성하는데 정말 많은 비용이 발생한다면 당연히 한번 생성한 객체를 재사용할 수 있도록 해야 한다.  

(성능 최적화를 위해 한번 생성한 위젯을 재사용하는 경우도 있으나 자주 있는 일은 아니다 - 성능 최적화 관련 포스팅에서 따로 다루도록 하겠다.)

하지만 그 비용이 현저하게 적다면 그리고 Flutter에서 위젯의 생성 비용을 최소화하고 내부적으로 연산이 복잡한 과정들(위젯의 위치, 크기 결정 등 렌더링에 필요한 연산)을 최적화해두었다면? 우리는 굳이 Widget을 재사용하기 위해서 읽기 힘든 복잡한 코드를 만드는 대신 위젯을 항상  생성해서 사용하면 될지도 모른다.

지금부터 Flutter에서 위젯 생성과 관련된 위 이슈를 어떻게  해결하고 있는지 확인해 보도록 하겠다.

Element가 Flutter 구조의 핵심이다. 

Flutter에서는 화면 구성과 동작을 위해 필요한 중요한 정보를 Widget이 아닌 Element를  통해서 유지하고 사용한다. 

아래 코드와 같이 모든 위젯은 자신과 연결될 Element를 생성(createElement) 해야 한다.  물론 재사용 가능한 element가 있을 경우 생성 없이 그 element을 재사용한다. 

재사용 가능 여부는 class method인 canUpdate()를 통해 확인하는데 이 값이 true인 경우 element를 재사용하고 그렇지 않은 경우 새로 생성한다.

element를  재사용한다는 것은 tree에 위치한 element를 그대로 사용한다는 뜻이며,   다시 생성한다는 것은 tree에서 기존 element를 제거(unmount) 하고 새로운 element를 추가(mount) 한다는 뜻이다. 

@immutable
abstract class Widget {
   …
  @factory
  Element createElement();
   …
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
  }
}
canUpdate 부분의 코드를 좀 더 유심히 살펴볼 필요가 있는데 widget의 타입과 key를 비교해서 같은지 검사하는 단순한 코드이지만, flutter에서 widget와 element의 관계를 이해하는데 중요한 개념을 담는 코드이다. (runtimeType이 같으며 key(null인 경우 포함)가 같으면 같은 위젯으로 보고 연결된 element를 그대로 다시 사용한다는 뜻)

 

위에서 설명했듯이 모든 Widget은 Element를 가진다. 

Element의 Lifecycle이 Widget보다 길기 때문에 Element가 Widget을 가진다로 표현해야 할지도 모르겠지만 서로 연결되어 있다가 더 정확한 표현인 것 같다. 

Widget은 단지 Element을 생성하고 변경하기 위해  개발자가  사용하기 쉬운 형태로 만들어진 설정 정도로 정의 하는 것이 Widget에 대한 올바른 정의일지도 모르겠다.

마치며 

Widget만 알아도 Flutter로 앱을 만드는데 아무런 지장이 없습니다.  커스텀 Widget과 기본 Widget들만 가지고도 충분히 사용할 만한 앱을 만들 수 있습니다.  

하지만 Element에 대한 이해가 없이 앱을 만들다 보면 의도치 않은 화면의 갱신이나 오동작을 만나게 됩니다. 

이때 Element에 대해서 알고 있다면 그 문제들을  쉽게 해결할 수 있으며, 처음부터 그런 오류가 없는 앱을 만들 수 도 있을 것입니다.

 

 

> 2부 - Widget Tree Element Tree 그리고 Lifecycle에서 계속

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함