[Flutter][번역] Flutter internals (2)

김영진·2021년 8월 4일
1

Flutter 앱 개발 일기

목록 보기
15/31

출처

내용

위젯에서 픽셀로

플러터는 모든것이 위젯이다

정확히 개발자의 관점에서는 레이아웃 및 유저와 관련된 모든것은 위젯을 통해 수행됩니다.

위젯을 사용하면 개발자가 화면의 일부를 디맨션, 컨탠츠, 레이아웃 및 상호작용 측면에서 정의할수 있기 때문에 훨씬 더 많은 기능이 있습니다.
실제로 위젯이란 정확히 무엇일까요?

불변 설정
플러터 소스코드를 읽으면 위젯 클래스는 다음과 같이 정의되어 있습니다.


abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  ...
}

@immutable 어노테이션은 매우 중요하며 위젯 클래스의 모든 변수는 FINAL이어야 합니다. 따라서 일단 인스턴스화 되면 위젯은 더이상 내부 변수를 조정할 수 업습니다.

플러터로 개발할때 아래와 같이 위젯을 사용하여 화면의 구조를 정의합니다.

Widget build(`BuildContext` context){
    return SafeArea(
        child: Scaffold(
            appBar: AppBar(
                title: Text('My title'),
            ),
            body: Container(
                child: Center(
                    child: Text('Centered Text'),
                ),
            ),
        ),
    );
}

이 예제는 7개의 위젯을 사용하여 구성되엇습니다.

SafeArea가 트리의 루트인것처럼 보입니다.

아시다시피 위젯 자체는 다른 위젯의 집합일 수 있습니다. 예를들어 이전 코드를 다음과 같이 작성할 수 있습니다.

Widget build(`BuildContext` context){
    return MyOwnWidget();
}

위 코드는 MyOwnWidget 이라는 위젯 자체가 SafeArea, Scaffold를 랜더링한다고 가정합니다.

트리에서 엘리먼트의 개념
제가 왜 이것을 언급했는지 생각해 보셨습니까?

나중에 알게되겟지만, 디바이스에 랜더링할 이미지를 구성하는 픽셀을 생성하기 위해서는 화면을 구성하는 모든 작은 부분을 자세하게 알아야하고, 모든 부분을 결정하려면 모든 위젯을 전개할것을 요청해야합니다.

이것을 설명하기 위해 러시아 인형의 원리를 생각해보면 처음에는 닫힌 인형만 보이지만 뚜껑을 열어보면 다른 인형에는 다른 인형이 포함되어 있는것을 볼 수 있습니다.

플러터가 일부에서 전체로 모든 위젯을 전개했을때 그것은 전체에서 일부인 다른 러시아 인형을 얻는것과 유사할 것이다.
다음 다이어그램은 이전 코드에 해당하는 풀 위젯 계층 구조의 일부를 보여줍니다. 노란색으로 코드에 언급된 위젯을 강조하여 무분 위젯 트리에서 찾을수 있도록 하였습니다.

위젯트리라는 말은 프로그래머들이 위젯을 사용하기 때문에 이해하기 쉽게 하기 위해서 사용하지만 플러터에서 실제로는 위젯트리가 없습니다.
그대신 엘리먼트 트리라고 말해야합니다.

이제 엘리먼트의 개념에 대해 알아볼 시간입니다.

각 위젯마다 하나의 엘리먼트에 해당합니다. 엘리먼트는 서로 연결되어 트리를 형성합니다. 따라서 엘리먼트는 트리에서 무언가를 참조합니다.

엘리먼트를 부모, 잠재적자식이 있는 노드로 생각하십시오. 부모 관계를 통해 서로 연결되어 트리 구조를 얻습니다.

엘리먼트는 하나의 위젯을 가리키고 랜더 오브젝트를 가리킬 수도 있습니다.

참고로 엘리먼트는 엘리먼트를 생성한 위젯을 참조합니다.

정리하면

  • 위젯트리는 없고 엘리먼트 트리는 있다.
  • 위젯에 의해 엘리먼트가 생성된다.
  • 엘리먼트는 위젯을 참조한다.
  • 엘리먼트는 부모와 연결되어있고 자식이 있을수 있다.
  • 엘리먼트는 랜더오브젝트를 참조할 수도 있다.

보시다시피 엘리먼트트리는 위젯과 랜더오브젝트 사이를 연결해 줍니다.

그렇다면 위젯이 엘리먼트를 생성하는 이유는 무엇일까요?

3개의 메인 카테고리 위젯들

플러터에서 위젯은 3가지 메인 카테고리로 나뉩니다. 개인적으로 카테고리를 다음과 같이 부릅니다.

  • The Proxies
    이 위젯의 주요 역할은 위젯에 제공해야하는 정보를 공유하는것입니다. 프록시 위젯이 트리의 특정 부분에서 루트가 되는 형식을 가집니다. 일반적인 예로 InheritWidget 또는 LayoutId가 있습니다.
  • The Renders
    이 위젯들은 화면의 레이아웃과 직접 관련이 있습니다.
    • Dimensions
    • Position
    • Layout, rendering
      일반적인 예는 다음과 같습니다.
    • Row, Column, Stack, Padding, Align, Opacity, RawImage ...
  • Components
    컴포넌트들은 디멘션, 포지션과 관련된 최정 정보를 직접 제공하는것이 아니라 최종 정보를 얻기 위해 사용될 데이터를 제공하는 위젯입니다.
    일반적인 예는 다음과 같습니다.
    • RaisedButton, Sacffold, Text, GestureDetector, Container...

카테고리별로 위젯을 나누는것이 중요한 이유는 엘리먼트의 타입과 관련이 있기 때문입니다.

Element types

Element 는 크게 두가지 타입으로 나눌 수 있습니다.

  • 컴포넌트 엘리먼트 : 이 엘리먼트는 시각적 랜더링 부분과 직접적으로 일치하지 않습니다.
  • 랜더오브젝트엘리먼트 : 이 엘리먼트는 랜더링된 화면의 일부에 해당합니다.

위젯과 엘리먼트는 어떻게 연동이 될까?

플러터 전체 메커니즘은 엘리먼트 또는 랜더러 오브젝트가 틀렸다고 하는데 있다.

엘리먼트를 틀렸다라고 하는 방법은 다음과 같다.

  • setState를 사용하여 Stateful엘리먼트를 틀렸다고한다.(sft위젯 아님)
  • 다른 proxyElement에 의해 전달되는 알림을 통해(예를들어 InheritedWidget), 해당 proxyElement에 의존하는 Element를 Invalidate합니다.

틀렸음의 결과는 해당 엘리먼트가 더러운 엘리먼트 리스트에서 찾을수 있다.

  • 디맨션, 포지션, 기하학 변경
  • 다시 그려야 하는경우(배경색 변경, 폰트변경)

이러한 틀렸음의 결과는 해당 랜더오브젝트가 다시 작성되거나 다시 그려져야 하는 랜더오브젝트 리스트에서 찾을 수 있다.

틀렸음의 유형에 관계없이 스케쥴러바인딩은 플러터 앤진에 새 프레임을 예약하도록 요청한다.
플러터 앤진이 스케쥴러 바인딩을 깨워 마술같은 일이 발생한다.

onDrowFrame()
이 글의 앞 부분에서 스케쥴러바인딩에서는 2가지 주요 역할이 있으며, 그중 하나는 프레임 재구축과 관련하여 플러터 앤진에서 생성한 요청을 처리할 준비가 되었음을 알려주는것이라고 언급했다.
이 단락에서 좀더 자세히 살펴보겠습니다.

아래의 시퀀스다이어그램은 스케쥴러 바인딩이 플러터앤진으로 부터 onDrawFrame()요청을 수신할때 발생하는 상황을 보여준다.

스텝1. 엘리먼트들
WidgetsBinding이 호출 된 후 먼저 엘리먼트와 관련된 변경사항을 체크합니다.

BuildOwner가 엘리먼트 트리 처리를 담당하므로 위젯 바인딩은 빌드 오너의 빌드 스코프 메소드를 호출합니다. 이 메소드는 틀렸음 된 엘리먼트목록(더러움리스트)를 순회하고 리빌드 요청하도록 한다.

rebuild() 메소드의 주요 역할을 다음과 같습니다.

  1. 엘리먼트를 리빌드 하도록 요청하면 대부분의 경우 엘리먼트가 참조하는 위젯의 build메소드를 호출합니다. (빌드 메소드는 새로운 위젯을 반환합니다)
  2. 엘리먼트에 자식이 없으면 새 위젯이 올라오고
  3. 새로운 위젯은 엘리먼트의 자식이 참조한 위젯과 비교합니다.
    3-1. 만약 위젯간의 타입과 키가 동일하다면 자식엘리먼트는 유지한채로 업데이트 됩니다.
    3-2. 만약 위젯간의 타입과 키가 동일하지 않다면, 자식 엘리먼트는 버려지고 새로운 위젯이 올라오게 됩니다.
  4. 위젯의 올라옴은 새로운 엘리먼트를 생성합니다. 그리고 엘리먼트의 새로운 자식으로 마운트 됩니다.(mounted = Element tree에 삽입)

다음 애니메이션은 위 과정을 좀 더 시각적으로 표현합니다.

위젯 올라옴 참고사항
위젯이 올라옴된 경우 위젯카테고리에 의해 정의된 특정 유형의 새로운 엘리먼트를 생성하도록 요청된다.
그러므로,

  • InheritedWidget은 InheritedElement를 생성
  • StatefulWidget은 StatefulElement를 생성
  • StatelessWidget은 StatelessElement를 생성
  • InheritedModel은 InheritedModelElement 생성
  • InheritedNotifier은 InheritedNotifierElement생성
  • LeafRenderObjectWidget은 LeafRenderObjectElement생성
  • SingleChildRenderObjectWidget 은 SingleChildRenderObjectElement 생성
  • MultiChildRenderObjectWidget 은 MultiChildRenderObjectElement 생성
  • ParentDataWidget 은 ParentDataElement 생성

이러한 엘리먼트는 카테고리별로 고유한 동작을 수행합니다.

예를들어

  • StatefulElement는 초기화시 widget.createState()메소드를 호출하여 State를 생성하고 element에 링크합니다.
  • RenderObjectElement 타입은 엘리먼트가 마운트될때 랜더 오브젝트를 생성합니다. 랜더 오브젝트는 랜더 트리에 추가되고 엘리먼트에 링크됩니다.

스텝2. 랜더오브젝트
더러운 엘리먼트와 관련된 모든 작업이 완료되면 엘리먼트트리는 이제 안정적이며 랜더링 과정을 고려할때입니다.
랜더러바인딩은 랜더링트리 처리를 담당하므로 위젯바인딩은 랜더러바인딩의 드로우프레임 메서드를 호출합니다.
아래 다이어그램은 드로우프레임 요청 중에 수행되는 일련의 동작들을 보여줍니다.

이단계 동안 다음 동작이 수행됩니다.

  • 더러움 표시된 랜더러오브젝트는 레이아웃을 수행하도록 요청됩니다. 디맨션, 도형을 계산하는것을 의미
  • 다시 그릴 필요가 있다고 표시된 랜더러 오브젝트는 렌더러 오브젝트의 레이어를 사용하여 다시 그리게 됩니다.
  • 결과 씬을 생성하여 플러터 앤진으로 전송하여 디바이스 화면으로 전송하도록 한다.
  • 마지막으로 시맨틱스도 업데이트 되어 플러터 앤진으로 전송된다

스텝3. 제스쳐 처리
제스쳐는 제스쳐바인딩에 의해 처리됩니다.
플러터 앤진이 제스쳐 관련 이벤트와 관련된 정보를 window.onPointerDataPacket API를 통해 보내면 제스쳐바인딩이 이를 가로채고 버퍼링을 진행하여 다음 과정을 수행합니다.
1. 플러터 앤진에서 출력된 좌표를 디바이스 픽셀 비율과 일치하도록 변환한 다음
2. 랜더러뷰에 이벤트 좌표를 포함하는 모든 랜더러 목록을 제공하도록 요청합니다.
3. 그런다음 해당 랜더러 오브젝트 목록을 순회하여 관련 이벤트를 각각에 전달합니다.
4. 랜더러 오브젝트가 해당 이벤트를 기다리고 있다면 그것을 처리합니다.
이 설명에서 랜더러 오브젝트가 얼마나 중요한지 확인 할 수 있습니다.

스텝4. 애니메이션
이 글의 마지막 부분은 애니매이션 개념, 특히 티커 개념에 중점을 둡니다.

애니메이션을 시작할때 일반적으로 애니메이션컨트롤러 또는 이와 유사한 위젯또는 구성 엘리먼트를 사용합니다.

플러터에서 애니메이션과 관련된 모든 것은 티커의 개념을 참고합니다.

티커는 활성화 된 경우 단 한가지만 수행합니다.
스케쥴러바인딩이 콜백을 등록하도록 요청하고 다음에 사용가능한 경우 플러터 앤진에 다시 호출하도록 요청합니다. 플러터 앤진이 준비되면 onBeginFrame요청을 통해 스케쥴러 바인딩을 호출합니다.

스케쥴러바인딩은 이 요청을 가로챈 다음 티커 콜백 목록을 순회하고 각각 아이템을 호출합니다.

각 티커 틱은 이 이벤트에 관심이있는 모든 컨트롤러에 인터셉트 되어 처리됩니다. 애니메이션이 완료되면 이커가 비활성화되고 그렇지않으면 티커가 다른 콜백을 예약하도록 스케쥴러바인딩을 요청합니다.

BuildContext

빌드컨텍스트는 앨리먼트에서 구현할 수 있는 일련의 게터와 방법을 조화롭게 정의하는 인터페이스 입니다.
특히 빌드컨텍스트는 stl위젯 및 stf위젯의 build메서드 또는 stf위젯 state객체에서 주로 사용됩니다.

buildContext는 그 자체를 나타내는 Element 자체입니다.

  • 빌드중인 위젯
  • 컨텍스트 변수를 참조하는 State에 연렬된 stf위젯

이는 대부분의 개발자가 모르는 상태에서도 지속적으로 앨리먼트를 처리하고 있음을 의미합니다.

어떻게 빌드컨텍스트를 사용할 수 있을까?
빌드컨텍스트는 위젯과 관련된 엘리먼트 뿐만 아니라 트리의 위젯 위치에도 해당하므로 빌드컨텍스트는 다음과 같은 경우에 매우 유용합니다.

  • 위젯에 해당하는 랜더오브젝트의 참조를 얻습니다.(위젯이 랜더러가 아닌경우 하위 위젯)
  • 랜더오브젝트의 사이즈를 얻을때
  • .of() 메소드를 구현하는 모든 위젯에서 실제로 사용됩니다.

우리는 빌드컨텍스트가 엘리먼트라는것을 이해했으므로 재미를 위해 그것을 사용하는 방법을 보여주고 싶었습니다.

아래 코드는 빌드컨텍스트를 사용하여 stl위젯이 setstate를 마치 사용한것처럼 자체적으로 업데이트 할수 있게 합니다.

void main(){
    runApp(MaterialApp(home: TestPage(),));
}

class TestPage extends StatelessWidget {
    // final because a Widget is immutable (remember?)
    final bag = {"first": true};

    
    Widget build(`BuildContext` context){
        return Scaffold(
            appBar: AppBar(title: Text('Stateless ??')),
            body: Container(
                child: Center(
                    child: GestureDetector(
                        child: Container(
                            width: 50.0,
                            height: 50.0,
                            color: bag["first"] ? Colors.red : Colors.blue,
                        ),
                        onTap:(){
                            bag["first"] = !bag["first"];
                            //
                            // This is the trick
                            //
                            (context as Element).markNeedsBuild();
                        }
                    ),
                ),
            ),
        );
    }
}

setState() 메소드를 호출할때 _element.markNeedBuild()와 같은 일을 합니다.

결론

저는 플러터가 어떻게 구성되었는지 아는것이 흥미로울수 있다고 생각했습니다. 그리고 모든것이 효율적이고 확장 가능하도록 설계되었음을 기억하기 바랍니다. 그리고 위젯 엘리먼트 빌드컨텍스트 랜더오브제트와 같은 핵심 개념들은 항상 이해하기 쉽지는 않습니다.

profile
2021.05.03) Flutter, BlockChain, Sports, StartUp

0개의 댓글