화면 구성 위젯
'위젯(widget)'은 화면에 보일 뷰(view)를 설명하는 객체이다. 즉, 화면과 관련된 모든 것이 '위젯'이다. 플러터는 '기본 위젯'과 '사용자정의 위젯'을 조합해 화면을 구성한다.
굉장히 기본적인 구조로 화면을 나타내보면 아래와 같다.
void main(){
runApp(
Center(
child: Text(
'flutter study',
),
),
);
}
위의 코드를 보면 플러터의 기본 위젯인 'Center'를 전달했다. 참고로 Center는 말 그대로 가운데 정렬을 뜻하는 위젯이다. 그리고 Center 위젯에 Text 위젯을 포함했다. Text 위젯은 말 그대로 텍스트를 나타낸다.
위의 runApp() 함수는, 전달받은 위젯을 '위젯 트리'의 '루트'로 만든다. 그리고 플러터 프레임워크는 루트 위젯으로 화면을 덮는다. 이처럼 플러터 앱의 화면은 필요한 위젯을 '계층'으로 나열해 구성한다.
실제 화면을 아주 간단하게 구현해보면 아래와 같다.
void main(){
runApp(MyApp());
}
class FlutterPrac extends StatelessWidget{
@override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('prac'),
),
body: Center(child: GestureDetector(child: Text('FlutterPrac'))),
),
);
}
}
위 코드의 FlutterPrac 클래스는 앱의 화면 전체를 구성하는 위젯으로 StatelessWidget 클래스를 상속받아 선언했다. StatelessWidget을 상속 받은 위젯은 화면에 보일 뷰를 갱신할 수 없다. 즉, '정적인 화면'이 된다.
위젯 클래스의 주요 작업은 다른 위젯을 계층으로 조합해 build()함수를 구현하는 것이다. 위의 코드는 7개의 위젯을 계층구조로 조합해 build()함수를 구현했다.
7계층은 아래와 같다.
플러터의 위젯은 Widget의 자식 클래스이다. Widget과 Text, Center 클래스의 계층을 예시로 보면 아래와 같다.
화면 구성 프로그래밍
플러터는 '선언형 프로그래밍'으로 화면을 구성한다. 선언형의 반대 개념은 '명령형 프로그래밍'이다.
'선언형 프로그래밍'은 많은 프런트엔드 프레임워크가 제공하는 기법이다. 실제로 플러터 공식 문서에는 플러터를 리액트 프레임워크에서 영감을 얻어 설계했다고 말한다.
'명령형'은 개발자가 화면 구성과 관련된 모든 코드를 작성하게 된다. 화면에 문자열을 출력할 때, 데이터에 더해 위치와 색상, 배경색 등의 정보를 코드로 작성해야 한다. 즉 많은 함수가 필요하다는 것이다. 그러면 명령형으로 화면을 구성할수록 코드가 길어질 수밖에 없고 화면과 관련된 많은 API를 알고 있어야 한다.
반면 플러터의 방식인 '선언형'은 화면 구성 정보만 작성하면 된다. 즉, 위젯의 정보만 제시하면 된다. 그러면 프레임워크가 알아서 API를 이용해 화면을 출력하게 된다.
위젯 불변
플러터의 위젯은 '불변 객체'이다. 객체를 생성한 후 상태를 바꿀 수 없다는 말이다. 따라서 처음 생성할 때 정보가 화면에 나온다. 만약에 화면을 새 데이터로 갱신하려면 새로운 위젯을 만들어야 한다.
※ 위젯은 위젯일 뿐 실제 화면이 아니다. 단순히 화면에 출력할 정보를 가지는 개체일 뿐, 실제 화면을 출력하는 객체는 아니다. 실제 화면을 출력하는 객체는 플러터 프레임워크에서 따로 만들어 사용하기에 화면을 갱신할 때 단순히 정보만 가진 객체를 다시 생성하는 것이다. 따라서 위젯 객체를 다시 생성하는게 비효율적인 것이 아니다.
위젯 트리 구조
대부분의 앱 화면은 위젯을 여러 개 조합해서 구성한다. 그리고 한 화면을 구성하는 모든 위젯은 '단일 트리 구조'를 이룬다.
트리 구조대로 화면 출력을 원하면 '루트 위젯 객체'만 runApp() 함수에 전달하면 된다. 그러면 자연스레 하위에 달린 위젯들까지 실행되어 화면을 구성하게 된다.
화면을 구성하는 3개의 트리 구조
위에 말한 바에 의하면 플러터는 화면을 만들 때 1개의 트리 구조가 만들어진다고 알겠지만, 사실은 2개의 트리 구조를 더 만들게 된다. 물론 개발자가 작성하는 위젯 트리는 아니다. 따라서 자세하게 알 필요까지는 없을 수 있지만, 플러터를 이해하기 위해서 알면 좋다. 2개 더 만들어지는 트리는 '엘리먼트 트리(element tree)'와 '렌더 트리(render tree)'이다.
위젯은 화면에 보일 뷰를 설명할 뿐이지 화면에 출력할 대상은 아니다. 그러니 프레임워크에서는 뷰 설명을 보고 위젯 트리를 참조해 실제 화면에 출력할 객체들을 별도의 트리 구조로 만들게 된다.
사실 코드만 보고 전체 위젯의 트리를 파악하는 것은 굉장히 어렵다. 이럴 땐, 안드로이드 스튜디오의 '플러터 인스펙터(flutter inspector)' 도구를 사용할 수 있다.
코드를 작성 후 안드로이드 스튜디오의 오른쪽 사이드 바에 있는 'Flutter Inspector'탭을 클릭하면 위젯의 트리 구조를 확인해볼 수 있다.하나를 클릭하면 상세 정보를 볼 수 있다.
예를 들어, Center -> Column -> Text 계층의 코드가 있따면, 실제 위젯 트리에서는 Text 하위에 RichText라는 위젯까지 만들어진다. Text에서 RichText 위젯으로 화면에 출력할 문자열을 표현하기 때문이다.
그리고 몇 개를 클릭해보면 이런 걸 알 수 있게 된다. 어떤 위젯은 renderObject 정보를 가지고 있고, 어떤 위젯은 가지고 있지 않다. renderObject를 포함하는 위젯이 실제 화면에 그릴 정보를 가진 위젯이다.
일단 아래 그림을 보고 이어서 설명해보겠다!
위젯 트리는 개발자가 작성한 코드에 기초해 만들어진다. 그리고 위젯 트리에서 노랑 바탕으로 표시한 위젯은 renderObject 정보를 포함한다.
플러터 플레임워크는 위젯 트리를 기반으로 엘리먼트 트리를 만든다. 위젯 트리를 보고 실제 화면을 구성하는 정보는 프레임워크 내부에서 엘리먼트 트리로 만든다. 위젯 객체 하나당 엘리먼트 객체를 하나씩 만들어 트리를 구성하고, 위젯보다 더 상세한 정보가 담겨져 있다.
엘리먼트 트리는 ComponentElement와 RenderObjectElement 객체로 구성된다. ComponentElement 객체는 트리 구조에서 다른 객체를 포함하는 역할만 하고, 화면에 출력할 정보를 가지지는 않는다. 실제 화면에 출력할 정보는 RenderObjectElement에 담기게 된다.
하지만 엘리먼트 트리의 객체도 실제 화면에 출력하지는 않는다. 단지 정보만 담고 있을 뿐이다. 엘리먼트 트리 정보를 바탕으로 실제 화면을 출력하는 렌더 트리가 만들어진다. 렌더 트리의 객체는 위젯 트리와 직접 연계되지는 않는다. 그리고 모든 엘리먼트 객체를 대상으로 렌더 트리의 객체가 하나씩 만들어지지도 않고, 실제 화면에 출력할 정보를 가지는 RenderObjectElement에 해당하는 객체로만 구성되게 된다.
렌더 트리를 구성하는 객체가 실제 화면을 출력하는 객체이다. 위의 그림에서는 RenderObject만 표현했지만, 실제로는 RenderObject 타입으로 표현되는 RenderDecorateBox, RenderImage, RenderFlex 등 다양한 객체가 사용된다.
※ 위와 같이 복잡한 트리 구조를 가지는 이유는 '화면 렌더링(출력) 속도' 때문이다.
플러터는 네이티브 앱 수준의 성능을 목표로 한다. 그렇기에 화면에 변화가 있을 때 최적의 알고리즘으로 변경할 부분만 다시 렌더링해서 빠르게 반영하도록 설계되어 있다. 이런 부분을 개발자가 신경쓰지 않게 하기 위해 위젯 트리 외에 엘리먼트 트리, 렌더 트리가 존재하는 것이다.