관리 메뉴

나만을 위한 블로그

[Flutter] 플러터 아키텍처 본문

개인 공부/Flutter

[Flutter] 플러터 아키텍처

참깨빵위에참깨빵 2024. 3. 31. 18:56
728x90
반응형

이 포스팅은 플러터가 어떤 구조인지 샅샅이 파악하려고 쓰는 게 아니다. 대략적으로 어떻게 만들어져 있는지 알아보는 포스팅이다.

포스팅의 바탕이 되는 사이트는 플러터 공식문서다. 너무 깊은 내용을 다루는 듯하거나 상관없어 보이는 내용은 생략했다.

 

https://docs.flutter.dev/resources/architectural-overview

 

Flutter architectural overview

A high-level overview of the architecture of Flutter, including the core principles and concepts that form its design.

docs.flutter.dev

플러터는 iOS, 안드로이드 같은 OS에서 코드 재사용을 허용하는 동시에 직접 인터페이스할 수 있도록 설계된 크로스 플랫폼 UI 도구 키트다. 플러터의 목표는 개발자가 서로 다른 플랫폼에서 자연스럽게 느껴지는 고성능 앱을 제공하고, 가능한 많은 코드를 공유하면서 존재하는 차이를 수용할 수 있도록 하는 것이다. 개발 중에 플러터 앱은 전체 재컴파일할 필요 없이 변경사항의 상태 저장 핫 리로드를 제공하는 VM(가상 머신)에서 실행된다...(중략)...이 개요는 여러 섹션으로 나뉜다

1. Layer model : 플러터가 구성되는 부분
2. 반응형 UI : 플러터 UI 개발의 핵심 개념
3. Widget 소개 : 플러터 UI의 기본 컴포넌트
4. 렌더링 프로세스 : 플러터가 UI 코드를 픽셀로 바꾸는 방법
5. Platform embedder 개요 : 모바일 및 데스크톱 OS에서 플러터 앱을 실행할 수 있게 해주는 코드
6. 플러터를 다른 코드와 통합 : 플러터 앱에서 쓸 수 있는 다양한 기술에 대한 정보
7. 웹 지원 : 브라우저 환경에서 플러터의 특성에 대한 결론

 

Architectural layers

 

플러터는 확장 가능한 계층화된 시스템으로 설계됐다. 기본 계층에 각각 의존하는 일련의 독립 라이브러리로 존재하고, 어떤 계층도 아래 계층에 대한 접근 권한이 없다. 또한 모든 부분은 선택 사항이고 교체 가능하다.

 

 

이제 각 계층이 뭐고 계층 별로 어떤 일을 하는지 확인해본다.

 

Embedder layer

 

접근성, 렌더링 표면, 입력 같은 작업을 위해 기본 OS와 조정하는 플랫폼 별 embedder가 진입점을 제공한다. embedder는 플랫폼 별 언어로 작성된다. 안드로이드는 자바와 C++, iOS와 맥OS는 Objective-C/C++이다.

플러터 코드는 이 embedder를 써서 모듈 또는 전체 앱의 컨텐츠로 기존 앱에 임베드하는 게 가능하다.

 

Engine layer

 

이 엔진은 C/C++로 작성된다. 입출력, 네트워크 요청, 렌더링 변환을 처리한다. "dart:ui"를 통해 플러터 프레임워크에 표시된다.

 

Framework layer

 

이 계층은 대부분의 개발자가 Dart 언어로 플러터와 상호작용하는 계층이다. 이 계층은 크게 아래 3가지와 플러터 앱 작성에 필요한 기본 클래스, 빌딩 블록 클래스(애니메이션, 그리기, 제스처 등)가 있다.

 

  • 렌더링
  • Widget
  • Material과 Cupertino

 


 

Widget은 플러터의 기본 컴포넌트인만큼 먼저 확인해 본다. Widget은 현재 구성과 상태가 표시되는 방식을 나타내고, Widget의 상태가 바뀌면 프레임워크가 이전 설명과 비교해 하나의 상태에서 다른 상태로 전환하기 위해 기본 렌더링 트리의 변경 사항을 확인하는 설명을 재작성한다. Widget은 버튼, 이미지, 아이콘, 레이아웃 등이 될 수 있고 위젯을 같이 배치하면 위젯 트리가 만들어진다.

 

위젯 트리는 화면에 존재하는 부모-자식 Widget의 무한한 체인이다. 레이아웃은 부모-자식 계층에서 Widget을 중첩해 만들어진다.

아래의 로그인 화면은 그 다음 이미지처럼 트리 형태로 나타낼 수 있다.

 

 

Root는 가장 기본이 되는 Widget으로 모든 Widget의 최상위에 위치한다. 그 밑으로 앱 테마인 Material Widget과 앱의 본체인 Scaffold Widget이 있다.

그리고 플러터의 Widget은 Layout Widget, 플랫폼 별 Widget, 독립 Widget, 상태 유지 관리 같은 범주로 그룹화할 수 있다. Widget을 함께 배치하는 프로세스를 구성(Composition)이라고 한다.

 

플러터 아키텍처 개요 문서에선 Widget을 아래와 같이 설명한다.

 

플러터는 Widget을 구성 단위로 강조한다. Widget은 플러터 앱 UI의 빌딩 블록이고 각 위젯은 UI의 변경 불가능한 선언이다. Widget은 구성(compositon)에 따라 계층 구조를 형성한다. 각 Widget은 부모 내부에 중첩되며, 부모로부터 컨텍스트를 받을 수 있다...(중략)...앱은 계층 구조의 위젯을 다른 위젯으로 대체하도록 프레임워크에 지시해서 이벤트(사용자 상호작용 등)에 대한 응답으로 UI를 업데이트한다. 그 다음 프레임워크는 새 Widget, 이전 Widget을 비교하고 UI를 업데이트한다. 플러터는 시스템에서 제공하는 걸 따르지 않고 각 UI 컨트롤의 자체 구현을 갖고 있다. 예를 들어 iOS 스위치 컨트롤과 이에 상응하는 안드로이드용 컨트롤 모두 순수 Dart 구현이 있다...(중략)

 

Composition

 

Widget은 일반적으로 결합해서 강한 효과를 만들어내는 다른 많고 작은 단일 목적 Widget으로 구성된다. 가능한 경우 디자인 개념의 수는 최소한으로 유지하면서 전체 어휘(vocabulary)를 크게 사용할 수 있게 한다. 예를 들어 위젯 레이어에서 플러터는 동일한 핵심 개념(위젯)을 써서 화면에 그리기, 레이아웃(위치 지정 및 크기 조정), 사용자 상호작용, 상태 관리, 테마 지정, 애니메이션, 탐색을 나타낸다...(중략)...클래스 계층 구조는 가능한 조합 수를 최대화하기 위해 의도적으로 얕고 넓으며 한 가지 일을 잘 수행하는 작고 구성 가능한 위젯에 중점을 둔다. 핵심 기능은 추상적이며 패딩, 정렬 같은 기본 기능도 코어에 내장되지 않고 별도의 컴포넌트로 구현된다. 이는 패딩 같은 기능이 모든 레이아웃 컴포넌트의 공통 코어에 내장된 기존 API와도 대조된다. 예를 들어 위젯을 중앙 배치하려면 개념적인 정렬 속성을 조정하는 대신 Center 위젯에 래핑한다...(중략)...플러터의 결정적인 특징은 모든 위젯의 소스를 드릴다운해서 검사할 수 있단 것이다. 따라서 사용자 지정 효과를 만들기 위해 컨테이너를 하위 클래스로 분류하는 대신 컨테이너와 다른 위젯을 새 방식으로 구성하거나 컨테이너를 영감을 얻어 새 위젯을 만들 수 있다

 

Building Widgets

 

앞서 말했듯이 새 요소 트리를 반환하도록 build()를 재정의해서 위젯의 시각적 표현을 결정한다. 이 트리는 위젯이 UI에서 차지하는 부분을 더 구체적으로 나타낸다. 예를 들어 도구 모음(toolbar) 위젯에는 일부 텍스트와 다양한 버튼의 가로 레이아웃을 반환하는 빌드 기능이 있을 수 있다. 필요에 따라 프레임워크는 트리가 렌더링 가능한 구체적인 객체로 완전히 설명될 때까지 각 위젯에 재귀적으로 빌드를 요청한다. 그 다음 프레임워크는 렌더링 가능한 객체를 렌더링 가능한 객체 트리로 연결한다. 위젯의 빌드 기능은 사이드 이펙트가 없어야 한다. 함수가 빌드를 요청할 때마다 위젯은 이전에 위젯이 반환한 것과 상관없이 새로운 위젯 트리1을 반환해야 한다...(중략)...렌더링된 각 프레임에서 플러터는 해당 위젯의 build()를 호출해서 상태가 변경된 UI만 다시 만들 수 있다. 따라서 build()는 빠르게 반환돼야 하고 무거운 계산 작업을 비동기 방식으로 수행한 다음 build()에서 사용할 상태의 일부로 저장해야 한다. 접근 방식이 상대적으로 단순하지만 이 자동화된 비교는 효과적이어서 고성능 대화형 앱을 구현할 수 있다. 또한 build()의 디자인은 하나의 상태에서 다른 상태로 UI를 업데이트하는 복잡한 작업 대신 위젯의 구성요소를 선언하는 데 집중해서 코드를 단순화한다

 

Widget State

 

프레임워크는 위젯의 2가지 주요 클래스인 stateful 및 stateless 위젯을 도입한다. 많은 위젯에는 변경 가능한 상태가 없다. 시간이 지나면서 변경되는 속성(아이콘 또는 레이블)이 없다. 이런 위젯은 StatelessWidget의 하위 클래스다. 그러나 위젯의 고유한 특성이 사용자 상호 작용 또는 기타 요인에 변경돼야 하는 경우 해당 위젯은 stateful이다. 예를 들어 위젯에 사용자가 버튼을 누를 때마다 증가하는 카운터가 있는 경우, 카운터 값은 해당 위젯의 상태다. 해당 값이 바뀌면 UI의 해당 부분을 업데이트하기 위해 위젯을 재빌드해야 한다. 이런 위젯은 StatefulWidget을 하위 클래스로 만들고 State를 하위 클래스로 만드는 별도의 클래스에 변경 가능한 상태를 저장한다. StatefulWidget에는 build()가 없다. 대신 UI는 State를 통해 구축된다. State 객체를 바꿀 때마다 setState()를 호출해서 State의 build()를 다시 호출해 UI를 업데이트하도록 프레임워크에 신호를 보내야 한다. 별도의 State 및 Widget 객체를 사용하면 다른 위젯이 상태 손실을 걱정하지 않고 정확히 동일한 방식으로 Stateless 위젯과 Stateful 위젯을 모두 처리할 수 있다. 상태 유지를 위해 자식을 붙잡을 필요 없이 부모는 자식의 지속적인 상태를 잃지 않고 언제든 자식의 새 인스턴스를 만들 수 있다. 프레임워크는 적절한 경우 기존 State 객체를 찾고 재사용하는 모든 작업을 수행한다

 

State management

 

많은 위젯이 상태를 포함할 수 있는 경우 상태는 어떻게 관리되고 시스템에서 전달되는가? 다른 클래스와 마찬가지로 위젯의 생성자를 써서 데이터를 초기화할 수 있으므로 build()는 자식 위젯이 필요한 데이터로 인스턴스화되도록 할 수 있다
@override
Widget build(BuildContext context) {
   return ContentWidget(importantState);
}
그러나 위젯 트리가 깊어짐에 따라 트리 계층에서 상태 정보를 위아래로 전달하는 게 번거로워진다. 따라서 3번째 위젯 유형인 InheritedWidget은 공유 조상에서 데이터를 가져오는 쉬운 방법을 제공한다. 아래와 같이 InheritedWidget을 써서 위젯 트리에서 공통 조상을 래핑하는 위젯을 만들 수 있다

ExamWidget 또는 GradeWidget 객체 중 하나가 StudentState 데이터를 필요로 할 때마다 아래 코드로 접근할 수 있다
final studentState = StudentState.of(context);
of(context) 호출은 빌드 컨텍스트(현재 위젯 위치에 대한 핸들)을 취하고 트리에서 StudentState 유형과 일치하는 가장 가까운 조상을 반환한다. InheritedWidget은 상태 변경이 이를 사용하는 자식 위젯의 rebuild를 트리거해야 하는지 여부를 결정하기 위해 플러터가 제공하는 updateShouldNotify()도 제공한다...(중략)

 

Flutter's rendering model

 

플러터가 크로스 플랫폼 프레임워크라면 어떻게 플러터가 단일 플랫폼 프레임워크와 비슷한 성능을 제공할 수 있는가? 기존 안드로이드 앱의 작동 방식을 생각하는 것부터 시작하는 게 좋다. 렌더링 시 먼저 안드로이드 프레임워크의 자바 코드를 호출한다. 안드로이드 시스템 라이브러리는 Canvas 객체에 자신을 그리는 컴포넌트를 제공하며 안드로이드는 CPU 또는 GPU를 호출해서 기기에서 render를 완료하는 C/C++로 작성된 그래픽 엔진 Skia로 렌더링할 수 있다. 크로스 플랫폼 프레임워크는 일반적으로 기본 안드로이드 및 iOS UI 라이브러리에 추상화 계층을 만들어 각 플랫폼 표현의 불일치를 완화하는 식으로 작동한다. 앱 코드는 종종 자바 기반 안드로이드 또는 Objective-C 기반 iOS 시스템 라이브러리와 상호작용해서 UI를 표시해야 하는 자바스크립트 같은 인터프리터 언어로 작성된다. 이 모든 것은 특히 UI와 앱 로직 사이에 많은 상호작용이 있는 경우 상당한 오버헤드를 가져온다. 플러터는 자체 위젯 세트를 선호해서 시스템 UI 위젯 라이브러리를 우회해서 이런 추상화를 최소화한다. Dart 코드는 렌더링에 Skia를 쓰는 네이티브 코드로 컴파일된다. 또한 플러터는 자체 Skia 사본을 엔진의 일부로 포함해서 개발자가 앱을 업그레이드해서 핸드폰이 새 안드로이드 버전으로 업데이트되지 않았어도 최신 성능 개선 사항으로 업데이트된 상태를 유지할 수 있다. iOS, 윈도우, 맥OS 같은 다른 기본 플랫폼의 플러터도 마찬가지다

 

From user input to the GPU

 

플러터가 렌더링 파이프라인에 적용하는 최우선 원칙은 단순함이 빠르다(simple is fast)는 것이다. 플러터에는 아래의 시퀀싱 다이어그램 같이 데이터가 시스템으로 흐르는 방식에 대한 파이프라인이 있다

- Build : 위젯에서 요소로

아래의 위젯 계층 구조를 보여주는 코드를 참고하라
Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);
플러터가 이 프래그먼트를 렌더링해야 할 때 현재 앱 상태를 기반으로 UI를 렌더링하는 위젯의 하위 트리를 반환하는 build()를 호출한다. 이 프로세스 중에 build()는 상태와 필요에 따라 새 위젯을 도입할 수 있다....(중략)...따라서 최종 위젯 계층 구조는 아래와 같이 코드가 나타내는 것보다 더 깊을 수 있다

(중략)...빌드 단계에서 플러터는 코드로 표현된 위젯을 모든 위젯에 대해 하나의 요소가 있는 해당 요소 트리로 변환한다. 각 요소는 트리 계층 구조의 지정된 위치에 있는 위젯의 특정 인스턴스를 나타낸다. 2가지 기본 유형의 요소가 있다

- ComponentElement : 다른 요소의 호스트
- RenderObjectElement : 레이아웃 또는 paint 단계에 참여하는 요소

 

RenderObjectElements는 위젯 아날로그와 나중에 확인할 기본 RenderObject 사이의 중개자다. 모든 위젯의 요소는 트리에서 위젯 위치에 대한 핸들인 BuildContext를 통해 참조할 수 있다. 이는 Theme.of(context) 같은 함수 호출의 컨텍스트이며 build()에 매개변수로 제공된다. 위젯은 노드 간의 부모/자식 관계를 포함해 변경할 수 없기 때문에 위젯 트리를 변경하면 새 위젯 객체 집합이 반환된다. 그러나 이것이 기본 표현을 다시 작성해야 한다는 의미는 아니다. 요소 트리는 프레임마다 지속되므로 플러터가 기본 표현을 캐싱하면서 위젯 계층 구조가 완전히 1회용인 것처럼 작동할 수 있는 중요한 성능 역할을 한다. 바뀐 위젯을 보기만 하면 플러터는 재구성이 필요한 요소 트리 부분만 다시 빌드할 수 있다

 

Integrating with other code

 

플러터는 코틀린, 스위프트 같은 언어로 작성된 코드 or API에 접근하거나 네이티브 C 기반 API 호출, 플러터 앱에 네이티브 컨트롤을 포함, 기존 앱에 플러터를 포함할 때 다양한 상호 운용성 매커니즘을 제공한다.
모바일 및 데스크탑 앱의 경우 플러터를 쓰면 플랫폼 채널을 통해 사용자 지정 코드를 호출할 수 있는데 이는 Dart 코드와  호스트 앱의 플랫폼별 코드 간 통신하는 매커니즘이다. 공통 채널(이름, 코덱을 캡슐화)을 생성하면 Dart와 코틀린, 스위프트 같은 언어로 작성된 플랫폼 구성요소 간에 메시지를 주고받을 수 있다. 데이터는 Map 같은 Dart 자료형에서 표준 형식으로 직렬화된 다음 코틀린 또는 스위프트의 동일한 표현으로 역직렬화된다

 

 

반응형
Comments