aspe

Flutter - Inside Flutter 본문

Flutter/근본

Flutter - Inside Flutter

aspe 2023. 2. 17. 14:03

Aggressive composability

Flutter의 가장 두드러지는 특징 중 하나는 aggressive composability 입니다. 위젯들은 더 기본 위젯으로 빌드 된 위젯들을 결합하면서 빌드됩니다. 예를 들면, Padding은 위젯의 특성이 아니라, 하나의 위젯입니다. 결과적으로, Flutter로 만들어진 유저 인터페이스는 매우 많은 위젯들로 구성됩니다.

 

위젯 빌딩을 수행하는 재귀작업은 내재된 렌더 트리에 노드를 생성하는 위젯인 RenderObjectWidgets에서 끝납니다. 렌더 트리는 layout 하는 동안 계산되고 painting, hit testing에서 사용되는 유저 인터페이스의 기하학적 정보를 저장하는 자료 구조입니다. 대부분의 Flutter 개발자들은 객체를 직접 렌더링하지 않고 위젯을 사용하여 렌더 트리를 조작합니다.

 

위젯 레이어에서 Aggressive composability를 지원하기 위해, Flutter는 위젯, 렌더 트리 레이어에서 효율적인 알고리즘과 최적화를 사용합니다.

Sublinear layout

수 많은 위젯과 렌더 객체들을 다루기 위해서, 성능에 가장 중요한 키는 효율적인 알고리즘 입니다. 가장 중요한 것은 layout의 성능입니다. layout은 렌더 객체의 기하학적 정보(예를 들면 크기, 위치)를 결정하는 알고리즘 입니다. 다른 toolkit들은 O(N^2)거나 더 좋지 않은 성능의 layout 알고리즘을 사용합니다. Flutter는 첫 번째 layout에 선형적인 성능을, 그 다음 이어오는 layout의 업데이트에는 준선형 layout 성능을 갖추었습니다. 일반적으로, layout에 소모되는 시간은 렌더 객체 수보다 느리게 조정되어야 합니다.

 

Flutter는 프레임 당 하나의 layout을 수행합니다. 그리고 layout 알고리즘은 싱글패스로 작동합니다. Constraints는 부모 객체로 부터 자식 들로 layout 메서드를 호출하며 내려갑니다. 자식들은 재귀적으로 자신의 layout을 수행하고 layout 메서드로 부터 반환된 기하학적 정보를 위로 보냅니다. 더 중요한 것은,렌더 객체가 layout 메서드로부터 반환되면, 해당 렌더 객체는 다음 프레임의 layout까지 다시 방문되지 않습니다. 이러한 접근 방식은 별도의 측정값과 layout 패스를 싱글 패스로 결합하며, 결과적으로 각 렌더 객체는 layout 중에 최대 두 번 방문됩니다. 한 번은 트리를 내려가는 도중에, 한 번은 트리를 올라가는 도중에.

 

Flutter는 일반화된 프로토콜의 몇 가지 특수화를 가지고 있습니다. 가장 일반적인 특수화는 RenderBox로 직교 좌표나 2D에서 동작합니다. 박스 레이아웃에서, constraints는 너비와 높이의 최대, 최소값 입니다. layout하는 동안, 자식은 이 최대 최소값 안에서 자신의 기하학적 정보를 결정합니다. 자식이 layout으로부터 반환되면, 부모는 부모의 좌표계에서 자식의 위치를 결정합니다. 자식의 layout은 자신의 위치가 자식이 layout으로부터 반환 될 때까지 결정되지 않으므로 스스로의 위치에 의존할 수 없다는 것을 알아두세요. 결과적으로, 부모는 자식의 layout을 다시 계산하지 않으므로써 자식을 재배치 시키는 것으로부터 자유로워집니다.

 

보다 일반적으로 layout하는 동안, 부모로부터 자식으로 가는 정보는 오직 constraints이고 자식으로부터 부모로 가는 정보는 오직 기하학적 정보입니다. 이런 불변함은 layout하는 동안 처리량을 줄여줍니다.

  • 만약 자식의 layout이 dirty로 표시되어있지 않다면, 부모가 이전 layout에서 받은 것과 동일한 constraints를 자식에게 주면, 자식은 layout에서 즉시 반환될 수 있습니다.
  • 부모가 자식의 layout 메서드를 호출할 때마다, 부모는 자식으로부터 반환되는 사이즈 정보를 사용할 것인지 결정합니다. 만약, 부모가 사이즈 정보를 사용하지 않는다면, 새로운 사이즈가 기존 constraints를 준수하도록 부모가 보장하므로 자식이 새 사이즈를 선택해도 부모가 layout을 다시 계산할 필요가 없습니다.
  • 타이트한 제약조건은 정확히 하나의 유효한 기하학적 정보로 이루어집니다. 예를 들면, 만약 너비의 최소, 최대값이 서로 같고 높이의 최소, 최대값이 서로 같다면, 해당 constraints를 만족하는 너비와 높이는 단 하나뿐입니다. 만약 부모가 tight contraints를 준다면, 부모는 자식은 부모의 새로운 constraints로 부터 사이즈를 변경 할 수 없기 때문에 부모가 자식의 사이즈를 layout에서 사용한다 할 지라도 자식이 자신의 layout을 다시 계산 할 때마다 본인의 layout도 계산 할 필요가 없습니다.

렌더 객체는 오직 자신의 기하학적 정보를 결정하기 위해 부모로부터 제공되는 constraints를 사용한다고 선언할 수 있습니다. 그런 선언은 자식이 부모의 새로운 contraints 없이 사이즈를 바꾸지 못하기 때문에 constrains가 tight하지 않아고 자식의 사이즈에 부모의 layout이 의존할 지라도, 해당 렌더 객체의 부모가 자신의 layout을 다시 계산할 필요가 없다고 프레임워크에게 알리는 것 입니다.

 

이런 최적화들의 결과로, 렌더 객체 트리가 dirty 노드를 가지고 있을 때, layout하는 동안 오직 해당 노드들과 그들 주변의 서브트리의 제한된 부분만을 방문하면 됩니다.

Sublinear widget building

layout 알고리즘과 비슷하게, Flutter의 위젯 building 알고리즘은 준선형입니다. build가 끝나고 위젯들은 유저 인터페이스의 논리적 구조를 가지고 있는 element tree에 홀드 됩니다. element tree는 위젯 스스로가 불변하기 때문에 필요합니다. 위젯이 불변하다는 것은 위젯들이 다른 위젯들과의 부모 자식 관계를 기억하지 못한다는 것입니다. 또한 element tree는 stateful 위젯과 연관된 state 객체를 홀드합니다.

 

유저의 input에 대한 응답으로 element는 dirty가 될 수 있습니다. 예를 들자면 만약 개발자가 연관된 state 객체들에서 setState()를 호출하는 경우가 있습니다. 프레임워크는 dirty element의 리스트를 보관하고 build 단계에서 다른 clean elements들은 스킵하고 바로 점프하게 됩니다. build 단계에서 정보는 element tree의 단방향으로 흐르고 그것은 각 element가 build 단계에서 많아봐야 한 번만 방문된다는 것을 의미합니다. dirty가 clean이 되면, element는 조상 elements들 또한 clean되기 때문에 다시 dirty가 될 수 없습니다.

 

위젯은 불변하기 때문에 만약 element가 dirty로 표시되어있지 않다면, 부모가 동일한 위젯으로 element를 다시 빌드 할 때 element는 build에서 즉시 반환될 수 있습니다. 게다가, element는 새로운 위젯과 이전의 위젯이 동일한지 확인하기 위해 두 위젯 레퍼런스의 객체 동일성만 확인하면 됩니다. 개발자는 이런 최적화를 reprojection 패턴을 구현하는 데 사용할 수 있습니다. reprojection 패턴에서 위젯은 build에서 멤버 변수로 저장된 prebuilt 자식 위젯을 포함합니다.

 

build하는 동안, Flutter는 InheritedWidgets을 사용해 부모 체인을 방문하는 작업을 피합니다. 만약 위젯들이 부모의 체인을 방문하는 작업을 다 수행해야 한다면, aggressive composition 때문에 꽤 오래걸리는 작업이 될 수 있습니다.(예를 들면 현재 컬러 테마를 정하는 일이라면, build 단계는 O(N^2)의 시간 복잡도가 됩니다. N은 트리의 깊이). 이런 불필요한 작업들을 피하기 위해, 프레임워크는 각 element에서 InheritedWidgets의 해쉬 테이블을 유지하므로써 정보를 element tree의 밑으로 전달합니다. 일반적으로, 많은 elements들이 같은 해쉬 테이블을 참조할 것이고 그것은 오직 새로운 InheritedWidget을 생산하는 elements에서 변경 될 것입니다.

Linear reconciliation

많은 인기과는 반대로, Flutter는 tree-diffing 알고리즘을 사용하지 않습니다. 대신, 프레임워크는 O(N) 알고리즘을 사용하여 각 element들이 독립적으로 자식 리스트를 관리하므로써 element들을 재사용할지 결정합니다. 자식 리스트 조화 알고리즘은 다음 케이스들을 최적화합니다.

  • 오래된 자식 리스트가 비어있을 때
  • 두 리스트가 동일할 때
  • 리스트의 정확히 한 장소에 하나 혹은 더 많은 위젯들의 삽입과 제거가 있을 때
  • 만약 각 리스트가 같은 키를 가지는 위젯을 포함한다면, 두 위젯은 매치됩니다.

일반적인 접근 방식은 각 위젯의 런타임 유형과 키를 비교하여 두 자식 리스트의 시작과 끝을 일치시키는 것이며, 일치하지 않는 모든 자식 리스트가 포함된 각 리스트 가운데 비어 있지 않은 범위를 찾을 수 있습니다. 그 다음 프레임워크는 이전 자식 리스트의 범위에 있는 자식들을 키에 따라 해쉬 테이블에 배치합니다. 그런 다음 프레임워크는 새 자식 리스트의 범위를 이동하고 해쉬 테이블을 키별로 쿼리하여 일치 여부를 확인합니다. 일치하지 않는 자식들은 삭제되고 처음부터 다시 빌드되는 반면 일치하는 자식들은 새 위젯으로 다시 빌드됩니다.

Tree surgery

Elements들을 재사용하는 것은 element가 stateful 위젯의 state와 내제된 렌더 객체를 가지기 때문에 성능에 매우 중요한 역할을 합니다. 프레임워크가 element를 재사용 할 수 있을 때, 해당 유저 인터페이스의 논리적 부분을 위한 state는 보존되고 이전에 계산된 레이아웃 정보는 재사용됩니다. 사실 elements를 재사용하는 것은 state와 layout 정보를 보존하는 non-local tree mutations(비국소적 트리 변형)을 지원하는 Flutter에게는 매우 가치가 있습니다.

 

개발자들은 위젯들 중 하나를 GlobalKey로 묶으므로써 비국소적 트리 변형을 수행할 수 있습니다. 각 글로벌키는 어플리케이션을 통틀어 유일하며 thread-specific(thread에 private한) 해쉬 테이블에 의해 관리됩니다. build 단계에서 개발자는 위젯을 element tree의 특정 위치에 글로벌 키와 움직일 수 있습니다. 해당 위치에 새로운 element를 build하는 것이 아니라, 프레임워크는 해쉬테이블을 확인하고 전체 서브트리를 보존하며 기존 element의 부모를 이전의 위치에서 새로운 위치에 알맞게 변경합니다.

 

부모가 바뀐 서브트리에 있는 렌더 객체들은 렌더 트리에서 부모로 부터 오는 정보는 layout constraints가 유일하기 때문에 layout 정보를 보존하는 것이 가능합니다. 새로운 부모는 자식 리스트가 변경되었기 때문에 dirty 표시가 되지만, 만약 새로운 부모가 이전의 부모가 자식에게 줬던 constraints와 같은 constraints를 넘긴다면 자식은 layout에서 즉시 반환됩니다.

 

글로벌키와 non-local tree mutations은 hero trainsitions이나 navigation같은 효과를 만들기 위해 개발자들에게 광범위하게 사용됩니다.

Constant-factor optimizations

Aggressive composability의 달성은 위와 같이 알고리즘적인 최적화 외에도 다양한 constant-factor optimizations(상수 요소 최적화)에 의존합니다. 이 최적화는 위에서 논의된 주요 알고리즘의 마지막(leaves)에서 가장 중요합니다.

 

  • 자식과 모델에 구애받지 않음. 자식 리스트를 사용하는 대부분의 다른 툴킷들과 다르게, Flutter의 렌더 트리는 특정 자식 모델을 커밋하지 않습니다. 예를 들면, RenderBox 클래스는 firstChildnextSibling을 구체화하기 보다 visitChildren()이라는 추상 메서드를 가집니다. 많은 서브클래스들은 자식들의 리스트를 지원하기 보다 멤버 변수로 잡혀있는 오직 하나의 자식만을 지원합니다. 예를 들면, RenderPadding은 오직 하나의 자식을 가지며 결과적으로, 적은 실행시간을 소모하는 보다 간결한 layout 메서드를 가집니다.
  • Visual render tree, logical widget tree. Flutter에서 렌더 트리는 디바이스 독립적으로, 시각적 좌표계에서 작동합니다. 이는 x 좌표의 작은 값이 방향과는 무관하게 항상 왼쪽을 향한다는 것을 의미합니다. 위젯 트리는 보통 읽는 방향에 따라 시각적 해석이 달라지는 시작 및 끝 값을 사용하는 논리 좌표에서 작동합니다. 논리적인 좌표에서 시각적 좌표로의 변환은 위젯 트리와 렌더 트리 사이의 handoff(건내줌)으로 수행됩니다. 이런 접근 방법은 렌더 트리에서의 layout과 painting 계산은 위젯에서 렌더 트리로의 handoff보다 더 자주 발생하고 반복되는 좌표 변환을 피할 수 있기 때문에 더 효과적입니다.
  • 텍스트는 전문화된 랜더 객체에 의해 다뤄집니다. 많은 대부분의 랜더 객체들은 텍스트의 복잡함에 대해 알지 못합니다.(처리를 안 한다는 뜻) 대신, 텍스트는 RenderParagraph라는 전문화되고, 랜더 트리에서 leaf인 랜더 객체에 의해 다뤄집니다. text-aware 랜더 객체를 서브클래싱하기 보다, 개발자들은 composition을 사용해 텍스트를 유저 인터페이스로 통합합니다. 이런 패턴은 RenderParagraph가 자신의 부모가 같은 layout constraints를 제공하는 한 텍스트 레이아웃을 다시 계산하는 작업을 피할 수 있다는 것을 의미합니다.
  • 관찰가능한 객체. Flutter는 모델 관찰과 반응 패러다임을 모두 사용합니다. 명백히, 반응 패러다임이 지배적이지만, Flutter는 leaf 데이터 구조를 위해 관찰 가능한 모델 객체를 사용합니다. 예를 들면, Animation은 그들의 값이 바뀌면 옵져버 리스트에게 알립니다. Flutter는 이런 관찰 가능한 객체들을 위젯트리에서 랜더트리로 전달하고, 랜더 트리는 객체를 직접 관찰하고 트리가 변경될 때 파이프라인의 적절한 단계만 무효화합니다. 예를 들면, Animation<Color>에서 발생한 변화는 build, paint 단계를 모두 발생시키지 않고, paint 단계만을 발생시킵니다.

Aggressive composition에 의해 생성된 큰 트리들을 종합하고 합산하면 이러한 최적화는 성능에 상당한 영향을 미칩니다.

Separation of the Element and RenderObject trees

Flutter의 RenderObject와 Element(Widget) 트리는 같은 구조를 가집니다.(isomorphic)(엄밀하게 말하면, RenderObject 트리는 Element 트리의 부분집합 입니다.). 이를 단순화하는 방법은 이 둘을 하나의 트리로 합치는 것 입니다. 그러나, 실질적으로 이 둘을 분리된 트리로 가지고 있는 것은 몇가지 장점이 있습니다.

 

  • 성능. 레이아웃이 바뀔 때, 레이아웃 트리의 관련된 부분들만 변화해야 합니다. Composition 때문에, element 트리는 종종 스킵되어야 할 추가적인 노드들을 가지고 있습니다.
  • 명료함. 역할의 깔끔한 분담은 위젯 프로토콜과 랜더 객체 프로토콜이 테스팅의 짐과 버그의 위험성을 낮추고 API surface를 단순화하며 서로의 전문화된 역할을 잘 소화할 수 있게 합니다.
  • 타입 세이프티. 랜더 객체 트리는 자식들이 런타임동안 적절한 타입을 가진다는 것을 보장할 수있기 때문에 더 타입 세이프합니다. Composition 위젯들은 layout 도중에 사용되는 좌표계에 구애받지 않을 수 있고(예를 들면, 앱 모델의 일부를 노출하는 동일한 위젯을 box layout과 sliver layout 모두 사용할 수 있습니다.), 그러므로 element 트리에서 랜더 객체의 타입을 증명하는 것은 트리가 잘 작동하기 위해 필요합니다.

Infinite scrolling

Infinite scrolling 리스트는 툴킷에서 악명높게 구현이 어렵습니다. Flutter는 builder 패턴을 기반으로한 간단한 인터페이스로 Infinite scrolling 리스트를 지원합니다. builder 패턴에서 ListView는 그들이 유저의 스크롤링 동안 보여질 때 위젯을 빌드하기 위해 콜백을 사용합니다. 이런 기능을 지원하기 위해서는. viewport-aware layout(뷰포트의 크기를 알고있는 레이아웃)building widgets on demand(필요에 따라 위젯을 빌드)가 필요합니다.

Viewport-aware layout

Flutter의 대부분 요소들 처럼, 스크롤할 수 있는 위젯들은 composition으로 빌드됩니다. 스크롤 가능한 위젯의 밖은 Viewport입니다. Viewport는"안쪽이 더 큰" 박스입니다. 즉, viewport의 경계를 넘어 확장되고 뷰로 스크롤될 수 있습니다. 그러나, viewport는 RenderBox를 자식으로 가지지 않고 slivers라고 알려진 RenderSliver를 자식으로 가집니다. Slivers는 viewport-aware layout 프로토콜을 가집니다.

 

Sliver layout 프로토콜은 부모가 constraints를 자식에게 전달하고 그 대가로 기하학적 정보를 받는다는 점에서 box layout 프로토콜의 구조와 일치합니다. 그러나, constraint와 기하학적 정보가 두 프로토콜에서 다릅니다. Sliver 프로토콜에서, 자식들은 남아있는 볼 수 있는 공간의 양을 포함하는 viewport에 대한 정보를 받습니다. 그것들이 반환하는 기하학적 정보는 collapsible headers나 parallax 같은 다양한 스크롤과 관련된 효과를 가능하게 합니다.

 

다른 slivers는 viewport에서 사용가능한 공간을 다른 방법으로 채웁니다. 예를 들면, 자식들의 선형 리스트를 만드는 sliver는 자식들이 부족하거나 공간이 부족해 질 때까지 각 자식을 순서대로 배열합니다. 비슷하게, 자식들의 2D 그리드를 생성하는 sliver는 볼 수 있는 그리드의 일정 부분만을 채웁니다. 그들은 얼마나 공간이 보이는지 알고있기 때문에, slivers는 무한히 자식을 생성 할 수 있을 지라도 유한한 수의 자식들을 생성합니다.(공간의 양을 알고있기 때문에, 굳이 많이 만들 필요가 없다는 말)

 

Slivers는 맞춤 스크롤 레이아웃이나 효과를 만들 때 사용됩니다. 예를 들면, 단일 viewport는 선형 리스트와 그리드가 따라오는 collapsible 헤더를 가질 수 있습니다. 세 개의 slivers 모두 slivers layout 프로토콜을 통해 협력하여 viewport를 통해 이러한 자식들이 헤더, 리스트 또는 그리드에 속하는지 여부와 관계 없이 실제로 볼 수 있는 자식들만 생산합니다.

Building widgets on demand

Flutter에 만약 엄격한 build -> layout -> paint 파이프라인이 있다면, viewport를 통해 얼마나 많은 공간을 볼 수 있는지에 대한 정보는 layout 단계 동안만 사용 가능하기 때문에 앞에서 언급한 infinite scroll 리스트를 구현하기에는 충분하지 않을 수 있습니다. 추가적인 시스템 없이는 layout 단계가 너무 늦어져 공간을 채우는 데 필요한 위젯을 빌드할 수 없습니다. Flutter는 파이프라인의 build, layout 단계를 인터리빙(interleaving)하여 이 문제를 해결했습니다.(build 단계를 layout 단계에 끼워 넣는다.). Layout 단계의 어떤 시점에서라도, 프레임워크는 해당 위젯들이 현재 layout을 수행하고 있는 랜더 객채의 후손들이라면 새로운 위젯을 필요에 따라 build 할 수 있습니다.

 

인터리빙 build와 layout은 오직 build와 layout 알고리즘의 엄격한 정보 전파의 제어가 있기 때문에 가능합니다. 특히, build 단계에서, 정보는 오직 트리의 아래로 전파될 수 있습니다. 렌더 객체가 layout을 수행할 때 layout walk(작업 정도로 해석)는 해당 렌더 객체 아래의 하위 트리를 방문하지 않았습니다. 즉, 해당 서브트리에서 빌드에 의해 생성된 쓰기는 지금까지 레이아웃 계산에 들어간 정보를 무효화할 수 없습니다. 비슷하게, 레이아웃이 랜더 객체에서 반환되면, layout 동안 해당 랜더 객체는 다시 방문되지 않으며, 이는 후속 layout 계산에 의해 생성된 쓰기는 랜더 객체의 서브트리를 만드는 데 사용되는 정보를 무효화할 수 없다는 것을 의미합니다.

 

추가적으로, 스크롤 중에 element를 효율적으로 업데이트하고 element가 viewport의 가장자리에서 뷰로 스크롤되거나 viewport의 가장자리에서 뷰 밖으로 스크롤될 때 랜더 트리를 수정하려면 선형 조정(linear reconciliation)과 트리 조작(tree surgery)이 필수적입니다.

Specializing APIs to match the developer's mindset

Flutter의 노드들의 베이스 클래스인 Widget, Elements, RenderObjects 트리들은 자식 모델을 정의하지 않습니다. 이렇게 하면 각 노드를 해당 노드에 적용할 수 있는 자식 모델에 맞게 특수화(specialize)할 수 있습니다.

 

대부분의 Widget 객체들은 하나의 자식 Widget을 가지므로 하나의 child 파라미터를 가집니다. 몇몇 위젯들은 임의 개수의 자식을 가지며, 리스트로 된 자식들의 파라미터를 가집니다. 일부 위젯들은 자식을 가지지 않고, 메모리도 사용하지 않으며, 자식 파라미터도 가지지 않습니다. 유사하게, RenderObjects는 자식 모델에 해당하는 API를 가집니다. RenderImage는 leaf 노드로 자식을 가지지 않습니다 RenderPadding은 하나의 자식을 가지므로, 해당 자식의 포인터를 위한 저장소를 가집니다. RenderFlex는 임의의 수의 자식들을 가지며 linked list로 그들을 관리합니다.

 

드문 경우에, 더 복잡한 자식 모델들이 사용됩니다. RenderTable 랜더 객체의 생성자는 자식들의 배열들의 배열을 받으며, 클래스는 행과 열의 수를 제어하는 getter와 setter를 가지며, 개별 자식 배열을 x, y 좌표로 대체하고, 행을 추가하고, 자식 배열을 새로 제공하고, 전체 자식 리스트를 단일 배열과 열의 개수로 바꾸는 특정한 방법이 있습니다. 구현에서, 그 RenderTable 랜더 객체는 다른 랜더 객체들 처럼 linked list를 사용하지 않고 대신에 indexable 배열을 사용합니다.

 

Chip 위젯과 InputDecoration 객체는 관련된 컨트롤들에 존재하는 슬롯들과 매치하는 필드를 가집니다. 예를 들어, 첫 번째 자식을 prefix 값으로 정하고 두번째 자식을 suffix로 정의하는 것과 같이 모든 자식 모델이 semantics를 자식 리스트 위에 강제로 계층화시키는 경우, 전용(dedicated) 자식 모델은 이름이 있는 전용 속성을 대신 사용할 수 있도록 허용한다.

 

이런 유연함은 트리에 있는 각 노드들이 자신의 역할에 맞게 조작될 수 있게 해줍니다. 테이블에 셀을 삽입하여 다른 모든 셀을 감싸는 경우가 드물며, 마찬가지로 레퍼런스 대신 인덱스 별로 flex row에서 자식을 제거하려는 경우도 드뭅니다.

 

RenderParagraph 객체는 가장 극단적인 케이스 입니다: 자식들이 모두 다른 타입을 가집니다, TextSpan RenderParagraph의 경계에서, RenderObject 트리는 TextSpan 트리로 변환됩니다.

 

개발자의 기대에 부응하기 위해 API를 전문화하는 접근 방식은 자식 모델에만 적용되는 것이 아닙니다. 일부 다소 사소한 위젯들은 개발자가 문제애 대한 해결책을 찾을 때 사용할 수 있도록 특별히 존재합니다. 행이나 열에 공백을 추가하는 것은 Expanded 위젯과 크기가 0인 SizedBox 자식을 사용하는 방법을 알면 쉽게 수행할 수 있지만, Spacer 위젯으로도 그 효과를 낼 수 있으므로 그런 패턴을 찾을 필요가 없습니다. Spacer 위젯은 Expanded, SizedBox를 사용하여 직접 그러한 효과를 얻을 수 있습니다.

 

비슷하게, 위젯 서브트리를 숨기는 것은 build에서 서브트리를 포함하지 않는 것으로 쉽게 구현됩니다. 그러나, 개발자는 일반적으로 이런 역할을 해주는 위젯이 있기를 기대하고, 이런 패턴(숨기기 기능)을 사소하고 재사용이 가능한 위젯에 담기 위해 Visibility 위젯이 존재합니다.

Explicit arguments

UI 프레임워크는 각 클래스의 생성자 인자의 의미론적 뜻을 기억하기 힘든 많은 속성들을 가집니다. Flutter가 반응형 패러다임을 사용하기 때문에, Flutter의 build 메서드가 많은 생성자 호출을 가지는 것은 흔합니다. Dart는 named arguments을 활용하므로써, Flutter의 API는 이러한 build 메소드를 명확하고 이해할 수 있게 유지할 수 있습니다.

 

이런 패턴은 다수의 인자들을 가지는 메소드들로 확장 될 수 있고, 특히 boolean 인자들로 확장 될 수 있으며, 메소드의 홀로 있는 true, false 리터럴들은 항상 자체 문서화(문서가 없어도, 한 눈에 보았을 때 의미를 알 수 있다는 뜻) 되어 있습니다. 게다가, API에서 이중 부정으로 흔히 생기는 혼란을 피하기 위해, boolean 인자들과 속성들은 항상 positive 형태로 명명됩니다.(예를 들면, disabled: false로 쓰지 않고, enabled: true로 사용)

Paving over pitfalls

Flutter 프레임워크의 많은 부분에 사용된 기술은 error condition이 없는 API를 정의하기 위함입니다. 이 기술은 모든 에러 클래스들을 고려대상에서 제외합니다.

 

예를 들면, 보간 함수는 보간 중 한 쪽 또는 양쪽 끝이 null 일 수 있도록 합니다. 양쪽이 모두 null 값인 보간은 항상 null이고, 시작이 null이거나 끝이 null인 보간은 주어진 타입의 zero analog로 보간하는 것과 같습니다. 이것은 개발자가 실수로 보간 함수에 null 값을 넘겼을 때, 에러를 발생하는 것이 아니라 합리적인 결과를 얻을 수 있다는 것을 말합니다.

 

더 알맞는 예로 Flex 레이아웃 알고리즘이 있습니다. 이 레이아웃의 컨셉은 flex 랜더 객체에게 주어진 공간이 자식들에 의해 나누어지고, flex의 크기는 가능한 공간 전체가 됩니다. 오리지날 디자인에서, 무한한 공간을 제공하는 것은 실패하였습니다: 이것은 flex가 무한한 크기를 가진다는 것을 의미하고, 쓸데없는 레이아웃 배치입니다. 대신, API를 조절하여 flex 랜더 객체에 무한한 공간이 할당되면, 랜더 객체가 원하는 자식의 크기에 맞게 크기를 조정하여 가능한 에러 발생 케이스를 줄였습니다.

 

이런 접근법은 일관적이지 않는 데이터를 만드는 생성자를 가지는 것을 피할 수 있게 합니다. 예를 들면, PointerDownEvent 생성자는 PointerEvent의 down 속성이 false로 세팅되는 것을 허락하지 않습니다(자기 모순적인 상황). 대신, 그 생성자는 down 필드의 파라미터는 없고 항상 true로 설정됩니다.

 

일반적으로, 이런 접근법은 input 도메인의 모든 값들을 위한 의미있는 보간법을 정의하기 위함입니다. 가장 간단한 예는 Color 생성자 입니다. 범위를 넘어갈수도 있는 4개의 정수를 받는 대신에(1 red, 1 green, 1blue, 1 alpha), 각각의 비트를 의미하는 하나의 정수를 받아서(ARGB 순서로 0xFF42A5F5 이렇게) 어떤 input 값이 와도 유효한 값이 되게 합니다.

 

더 자세한 예는 paintImage() 함수 입니다. 이 함수는 11개의 인수를 가지고 있습니다. 몇몇은 꽤 넓은 입력 도메인을 가지고 있지만, 그것들은 대부분 서로 직교하도록 유효하지 않은 조합이 거의 없을 정도로 세심하게 설계되었습니다.

Reporting error cases aggressively

모든 오류 조건을 설계할 수 있는 것은 아닙니다. 디버그 빌드에서 Flutter는 일반적으로 오류를 매우 일찍 발견하려고 시도하고 즉시 보고합니다. Asserts가 주로 사용됩니다. 생성자 인자들은 자세하게 확인됩니다. 생명주기는 모니터되고 비일관성이 감지되면 그들은 즉시 예외를 발생시킵니다.

 

어떤 경우에, 이것은 극단으로 치닫습니다: 예를들어, 테스트가 수행하는 다른 작업에 관계없이 유닛 테스트를 실행할 때, 배치된 모든 RenderBox 서브클래스는 고유한 크기 조정 방법이 고유한 크기 조정 조건을 따르는지 여부를 적극적으로 검사합니다. 이렇게 하면 실행하지 않을 수 있는 API의 오류를 잡는데 도움이 됩니다.

 

예외가 발생하면, 예외들은 가능한 많은 정보를 가지고 있습니다. 몇몇 Flutter의 에러 메세지들은 실제 버그의 가장 가능성 있는 위치를 결정하기 위해 연관된 스택의 흔적을 미리 탐색합니다. 가장 흔한 에러는 에러를 방지하기 위한 샘플 코드나 추가 설명에 대한 링크를 포함한 자세한 지침을 포함합니다.

Reactive paradigm

변형 가능한 트리 기반 API들은 양분된 접근 패턴에 고통받습니다: 트리의 원래 상태 생성은 일반적으로 후속 업데이트와 매우 다른 작업 집합을 사용합니다. Flutter의 렌더링 레이어는 효율적인 layout과 painting의 키포인트인 영구 트리를 유지하는 효과적인 방법이기 때문에 이 패러다임을 사용합니다. 그러나, 랜더링 레이어와의 직접적인 상호작용은 기껏해야 awkward하고 최악의 경우에는 버그가 발생하기 쉽다는 것을 의합니다.

 

Flutter의 위젯 레이어는 내제된 랜더링 트리를 조작하기 위해 이 반응형 패러다임을 사용하는 compositon 메커니즘을 가지고 있습니다. 이 API는 트리 생성과 트리 변형 단계를 단일 트리 빌드 단계로 결합함으로써 트리 조작을 추상화합니다. 여기서 시스템 상태로 변경될 때마다 개발자는 유저 인터페이스의 새로운 configuration을 설명하고 프레임워크는 이 새로운 것을 반영하는 데 필요한 일련의 트리 변형을 계산합니다.

Comments