Flutter 아키텍처 개요 3편: 위젯
위젯 (Widgets)
플러터의 모든 것은 위젯이고, 사용자 인터페이스를 구성하는 기본 구성 요소이다. 그리고 위젯은 불변(immutable)한 선언이다.
위에서 말하는 불변한 선언이라는 의미가 무엇인지는 아래의 Widget 클래스의 코드를 보면 알 수 있다.
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
...
...
...
}
@immutable 어노테이션이 Widget 클래스에 붙어있는 것을 볼 수 있다. 해당 어노테이션이 클래스에 붙어 있으면 해당 클래스는 본질적으로 불변하기를 원한다는 의미이다. 따라서 해당 클래스의 모든 변수는 final 이어야 하고, 인스턴스화 된 클래스는 더 이상 내부의 값을 바꿀 수 없는 상태가 된다.
위젯은 계층 구조를 형성할 수 있고 각 위젯들은 부모 위젯 안에 중첩이 되며 부모로부터 Context를 받는다.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('My Home Page'),
),
body: Center(
child: Builder(
builder: (context) {
return Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Click!');
},
child: const Text('A button'),
),
],
);
},
),
),
),
);
}
}
위 MyApp 클래스에서 인스턴스화 된 모든 클래스는 위젯이다.
플러터 앱은 이벤트(사용자 입력 등..)에 반응하여 현재 구성되어 있는 위젯을 다른 위젯으로 교체하도록 지시하여 UI를 업데이트 한다. (프레임워크의 build() 함수 호출) 그러면 프레임워크에서는 새 위젯과 이전 위젯을 비교하여 효율적으로 UI를 업데이트 하게 된다.
플러터는 시스템(플랫폼, OS) 에서 제공하는 컨트롤에 의존하지 않고 자체적으로 UI 컨트롤들을 구현한다.
- 안드로이드의 머터리얼 디자인, IOS의 쿠퍼티노 디자인을 직접 구현하기 때문에 플랫폼과 통신하는 코드가 없어 빠르다.
- 직접 구현하는 이점으로 여러 플랫폼에서, 또는 플랫폼에서 컨트롤의 구현을 변경하더라도 완전히 동일한 UI를 경험할 수 있다.
- 개발자는 플랫폼에서 제공하는 컨트롤의 한계를 넘어 직접 기능의 확장이 가능하다.
구성 (Composition)

위젯은 일반적으로 여러 개의 작은 단일 목적의 위젯들의 구성(조합)으로 구성된다. 플러터에서는 이러한 위젯을 결합하여 큰 효과를 낼 수 있다.
패딩이나 정렬과 같은 기본적인 기능도 코어에 내장되어 있지 않고 별도의 위젯으로 구현된다. (이러한 부분은 다른 기존 API와는 대조적인 부분이다. 기존 API에는 일반적으로 패딩이나 레이아웃과 관련된 '속성'이 컴포넌트에 설정하는 방식으로 사용한다)
그래서 플러터에서는 어떤 위젯을 가운데에 배치하고 싶다면 Alignment 와 같은 속성값을 설정하는 게 아닌
Center 위젯으로 정렬을 원하는 위젯을 감싸면 된다. 플러터에는 "패딩, 정렬, 행, 열, 그리드" 에 대한 위젯을 기본적으로 제공하고 이러한 위젯들에는 자체적인 시각적 표현이 존재하지 않는다. 대신 다른 위젯의 레이아웃을 제어하는 기능이 있다.
그런데 Container 위젯을 보면 속성에 패딩도 있고 정렬 기능 등등의 기능이 다 있는 것을 볼 수 있는데, 이것도 Container 위젯 클래스의 코드를 보면 LimitedBox, ConstainedBox, Align, Padding, DecoratedBox, Transform 위젯들의 구성(조합)으로 되어있는 것을 볼 수 있다.
class Container extends StatelessWidget {
Container({
super.key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
})
...
...
...
@override
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null) {
current = Padding(padding: effectivePadding, child: current);
}
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
child: current,
);
}
if (decoration != null) {
current = DecoratedBox(decoration: decoration!, child: current);
}
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration!,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}
if (margin != null) {
current = Padding(padding: margin!, child: current);
}
if (transform != null) {
current = Transform(transform: transform!, alignment: transformAlignment, child: current);
}
return current!;
}
...
...
...
}
위젯 구축 (Building Widgets)
위젯은 build() 함수를 재정의하여 새 위젯 트리를 반환함으로서 시각적 표현을 결정할 수 있다.
위젯의 build() 함수는 부작용이 없어야 한다. build 함수가 호출이 될 때마다 이전 위젯의 반환값과 상관 없이 새로운 위젯 트리를 반환해야 한다.
프레임워크는 렌더링된 각 프레임에서 새 위젯 트리와 이전 위젯 트리를 비교하여 상태가 변경된 부분만 다시 생성할 수 있다. (build 함수 내부에서는 무거운 작업을 해서는 안 되며, 최악의 상황이라면 프레임 당 1번의 호출이 될 것이라는 가정을 해야한다.)
이러한 방식은 개발자가 직접 UI의 변경을 위해 변경된 부분을 업데이트 하는 복잡한 방식 대신에 단순히 위젯의 구성들을
선언하는데 집중하게 할 수 있고 코드를 단순하게 만들어준다.
위젯 상태 (Widget state)
플러터는 위젯에 "상태 저장 위젯 (StatefulWidget) 과 상태 비저장 위젯 (StatelessWidget)" 클래스를 도입했다.
대부분의 위젯을 생각해보면 변경 가능한 상태가 없는 경우가 대부분이다. 예를 들어서 아이콘, 레이블과 같은 UI 요소들은 시간이 지나도 변경되는 속성이 없다. 이러한 위젯들은 StatelessWidget 클래스를 상속 받아 만들어졌다.
그러나 CheckBox 위젯과 같이 사용자의 상호작용이나 기타 요인에 의해서 변경되어야 하는 경우 이러한 위젯은 상태 저장형 위젯이라고 할 수 있다. 이러한 상태 저장형 위젯은 StatefulWidget 클래스를 상속 받아서 만들고, 상태에 대한 정보 위해 State 클래스를 상속하는 별도의 클래스에 저장한다.
이렇게 하는 이유는 위젯은 불변 객체이기 때문에 위젯이 변경이 가능한 정보를 가질 수 없기 때문이다.
상태 관리 (State management)
그러면 어떠한 상태가 자식의 자식의 자식의 위젯으로 전달이 되어야 한다고 했을 때 어떻게 효과적으로 전달이 가능할까?
간단하게 생각해보면 어떠한 상태(데이터)를 위젯의 생성자를 통하여 전달할 수 있기 때문에 생성자에서 데이터를 받아 다시 build() 함수에서 자식 위젯으로 필요한 데이터를 전달하면 될것이다.
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
하지만 이러한 위젯 트리의 깊이가 깊어질수록 매우 번거로운 일이란 걸 알 수 있다. 따라서 세 번째 위젯 유형인 InheritedWidget 을 이용하면 조상 위젯으로부터 데이터를 쉽게 가져올 수 있는 방법을 제공한다.
아래 그림과 같이 InheritedWidget을 사용하여 위젯 트리에서 조상을 감싸는 상태 위젯을 만들 수 있다.

ExamWidget 또는 GradeWidget에서 StudentState의 데이터가 필요하면 아래와 같은 명령으로 가져올 수 있다.
final studentState = StudentState.of(context);
of(context) 는 빌드 컨텍스트로부터 트리에서 원하는 유형과 가장 가까운 조상을 찾아 반환하게 된다. 또 InheritedWidgets 은 업데이트를 위해 ShouldNotify() 함수를 제공하고 이 함수가 호출되면 , 이를 사용하는 자식 위젯을 리빌드해야 하는지 여부를 결정하게 된다.
InheritedWidgets 를 사용하면 대표적인 예로 앱의 테마와 같은 기능을 들 수 있다. 앱의 최상위에 테마와 관련된 위젯을 정의하고 하위 위젯에서 of(context) 을 통해 테마 위젯에 있는 속성들을 사용하여 앱을 제작할 수 있다.
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.titleLarge,
),
);
출처
https://docs.flutter.dev/resources/architectural-overview#widgets