Flutter 위젯 시스템은 Flutter 애플리케이션의 핵심이며, UI를 구성하는 기본 요소이다. 모든 것이 위젯으로 정의되며, Flutter 애플리케이션의 모든 시각적 요소는 위젯의 조합으로 이루어져 있다. 이러한 위젯 시스템은 재사용 가능하고 직관적인 구조로, 애플리케이션의 상태와 UI를 효율적으로 관리할 수 있게 해준다.
위젯의 기본 개념
Flutter에서 위젯은 화면에 나타나는 시각적인 요소를 나타낸다. 이러한 위젯들은 컨테이너, 텍스트, 버튼 등과 같은 간단한 것부터 더 복잡한 레이아웃 구조를 포함하는 복합 위젯까지 다양한다. 위젯의 특성은 크게 두 가지로 나눌 수 있다:
- Stateless 위젯: 상태를 가지지 않으며, 한 번 생성되면 이후에 변화하지 않는 위젯이다.
- Stateful 위젯: 자체적으로 상태를 관리하며, 사용자 입력이나 내부 로직에 따라 동적으로 변할 수 있는 위젯이다.
Stateless 위젯과 Stateful 위젯
Flutter는 위젯을 Stateless와 Stateful로 나누어 관리한다. 이 둘의 차이는 상태 관리 여부에 있으며, 이를 통해 Flutter는 성능 최적화와 유지보수성을 동시에 고려한다.
Stateless 위젯
Stateless 위젯은 상태가 없는 위젯이다. 한 번 생성되면 그 상태가 바뀌지 않으며, 사용자 입력에 따라 변하지 않는다. 예를 들어, 단순히 텍스트를 출력하는 위젯은 Stateless 위젯으로 구현될 수 있다.
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('This is a stateless widget');
}
}
Stateful 위젯
반대로 Stateful 위젯은 상태를 가지며, 상태의 변화에 따라 UI가 동적으로 업데이트된다. 이 위젯은 두 가지로 구성된다:
- StatefulWidget: 실제 위젯 구조를 나타내는 클래스.
- State: 상태 관리 로직을 포함하는 클래스.
Stateful 위젯은 다음과 같이 상태를 관리한다.
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
이 코드에서 setState() 메서드는 상태를 갱신하고, Flutter는 자동으로 화면을 다시 렌더링하여 UI를 업데이트한다.
위젯 트리와 렌더링
Flutter 애플리케이션은 위젯을 트리 구조로 관리한다. 각 위젯은 다른 위젯의 자식이 될 수 있으며, 이러한 트리 구조는 Flutter의 렌더링 엔진에서 UI를 그리는 데 사용된다. 위젯 트리는 부모-자식 관계를 명확하게 정의하며, 최상위 위젯에서 하위 위젯으로 UI 구조를 전달한다.
이와 같은 트리 구조에서 MyApp이 최상위 위젯이며, Scaffold는 화면의 전반적인 구조를 정의한다. 그 아래로 AppBar와 Body가 위치하며, Body 내부에는 Text와 Button과 같은 위젯이 포함된다.
위젯의 수명 주기
Stateful 위젯은 여러 단계의 수명 주기(lifecycle)를 거치며, 각 단계마다 적절한 메서드를 호출하여 상태를 관리한다. Flutter는 이러한 수명 주기를 통해 위젯의 생성, 상태 갱신, 소멸 과정을 관리한다.
Stateful 위젯의 주요 수명 주기 메서드는 다음과 같다:
- initState(): 위젯이 처음 생성될 때 호출된다. 여기에서 초기 상태를 설정할 수 있다.
- didChangeDependencies(): 위젯의 종속성이 변경될 때 호출된다. 이 메서드는 보통 위젯 트리에서 부모 위젯이 변경될 때 호출된다.
- build(): 위젯을 화면에 그리는 메서드이다. setState()가 호출되면 다시 호출된다.
- dispose(): 위젯이 더 이상 필요하지 않게 되었을 때 호출된다. 여기에서 리소스 해제를 수행한다.
수명 주기 예시 코드
아래의 코드에서는 각 수명 주기 메서드를 사용한 예시를 보여준다:
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState() {
super.initState();
print('initState called');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies called');
}
@override
Widget build(BuildContext context) {
return Text('Hello, Flutter!');
}
@override
void dispose() {
print('dispose called');
super.dispose();
}
}
이 코드에서는 initState, didChangeDependencies, build, dispose 메서드를 각각 사용하여 수명 주기의 흐름을 확인할 수 있다. 위젯이 생성될 때 initState가 호출되고, 위젯의 종속성이 변경될 때 didChangeDependencies가 호출된다. 위젯이 화면에 그려질 때 build가 호출되며, 위젯이 제거될 때 dispose가 호출된다.
위젯 조합과 레이아웃
Flutter의 위젯 시스템은 레이아웃을 만드는 데 유연하게 활용될 수 있다. Flutter에서 레이아웃을 구성하는 위젯은 부모 위젯의 크기와 자식 위젯의 크기를 결정한다. 위젯 조합을 통해 복잡한 UI를 설계할 수 있으며, 이를 효율적으로 처리하기 위한 다양한 레이아웃 위젯들이 제공된다.
대표적인 레이아웃 위젯
- Column: 자식 위젯들을 세로로 정렬한다.
- Row: 자식 위젯들을 가로로 정렬한다.
- Stack: 자식 위젯들을 겹쳐서 배치한다.
- Container: 자식 위젯의 크기, 여백, 테두리 등을 제어하는 다용도 위젯이다.
Column(
children: <Widget>[
Text('First Line'),
Text('Second Line'),
ElevatedButton(
onPressed: () {},
child: Text('Press me'),
),
],
);
위 코드에서는 Column 위젯을 사용하여 여러 자식 위젯을 세로로 나열하고 있다. Text 위젯과 ElevatedButton 위젯이 순서대로 정렬된다.
위젯의 재구성
Flutter의 위젯 재구성(rebuild)은 위젯 트리 내에서 UI가 변경될 때 중요한 과정이다. Stateful 위젯은 상태가 변경되면 setState()가 호출되고, 그에 따라 위젯의 일부가 재구성된다. 그러나 Stateless 위젯은 상태를 가지지 않으므로, 외부 입력이나 부모 위젯의 변화에 의해서만 재구성된다.
Flutter는 성능을 위해 효율적인 재구성 전략을 가지고 있다. 변경된 부분만 다시 렌더링하며, 나머지 부분은 그대로 유지된다. 이 방식은 앱이 복잡해지더라도 성능을 유지하는 데 중요한 역할을 한다.
위젯 트리의 최적화
Flutter에서 UI의 성능을 유지하려면 위젯 트리를 효율적으로 관리하는 것이 매우 중요하다. 위젯 트리가 너무 자주 재구성되거나, 불필요한 부분까지 재구성되는 경우 성능 저하가 발생할 수 있다. 이를 방지하기 위한 몇 가지 최적화 기법을 사용할 수 있다.
const 키워드
const 키워드는 위젯을 상수로 정의하여 해당 위젯이 재구성되지 않도록 하는 역할을 한다. 만약 위젯이 한 번 생성된 이후 변경되지 않는다면 const 키워드를 사용하여 불필요한 재구성을 방지할 수 있다. 다음은 const 키워드를 사용한 예시이다:
const Text('This is a constant text widget');
위 코드에서는 Text 위젯이 상수로 정의되었기 때문에 Flutter는 이 위젯을 다시 렌더링하지 않고, 성능 최적화를 이루게 된다.
Key의 사용
Flutter는 Key를 사용하여 위젯을 고유하게 식별할 수 있다. 위젯 트리 내에서 동일한 타입의 위젯이 여러 개 사용되는 경우, Flutter는 Key를 통해 위젯을 추적하여 올바르게 재구성할 수 있다. 예를 들어, 동적으로 생성된 리스트에서 위젯이 재배치될 때 Key를 사용하면 위치가 올바르게 유지된다.
ListView(
children: [
Container(key: Key('item1'), child: Text('Item 1')),
Container(key: Key('item2'), child: Text('Item 2')),
Container(key: Key('item3'), child: Text('Item 3')),
],
);
이와 같이 Key를 명시적으로 설정하면, Flutter는 트리 구조 내에서 각 위젯의 위치를 명확하게 인식하여 성능을 최적화한다.
BuildContext와 InheritedWidget
BuildContext는 Flutter의 위젯 트리에서 각 위젯의 위치를 나타내는 중요한 객체이다. 이를 통해 부모 위젯과의 관계를 파악하고, 하위 위젯에 데이터를 전달하거나 상호작용할 수 있다.
BuildContext의 역할
BuildContext는 위젯 트리의 구조를 관리하는 중요한 객체이다. 각 위젯은 BuildContext를 통해 자신의 부모, 자식 위젯과 소통하며, Navigator, Theme, MediaQuery 등 Flutter의 전역적인 객체에도 접근할 수 있다.
@override
Widget build(BuildContext context) {
return Text(
'Screen width: ${MediaQuery.of(context).size.width}',
);
}
위 코드에서는 MediaQuery를 통해 화면의 가로 크기를 가져오고 있으며, 이를 BuildContext를 통해 접근하고 있다.
InheritedWidget
InheritedWidget은 데이터를 하위 위젯에 효율적으로 전달하는 역할을 한다. Flutter는 InheritedWidget을 통해 상위 위젯에서 하위 위젯으로 데이터를 전파하고, 필요한 경우 위젯 트리에서 특정 위젯만을 재구성한다. 이 방식은 특히 상태 관리 라이브러리인 Provider에서 많이 사용된다.
class MyInheritedWidget extends InheritedWidget {
final int counter;
MyInheritedWidget({required this.counter, required Widget child}) : super(child: child);
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return oldWidget.counter != counter;
}
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
이 코드에서 MyInheritedWidget은 상위 위젯의 counter 값을 하위 위젯들에 전달하며, updateShouldNotify 메서드를 통해 변경된 경우에만 하위 위젯을 다시 렌더링한다.
위젯의 상태 관리
Flutter 애플리케이션에서 상태 관리는 매우 중요한 주제이다. 복잡한 애플리케이션일수록 많은 데이터와 사용자 상호작용을 관리해야 하며, 이러한 데이터를 효율적으로 처리하는 방법이 필요하다. Flutter는 다양한 상태 관리 방식을 지원한다.
setState()를 이용한 상태 관리
가장 기본적인 상태 관리 방법은 Stateful 위젯 내에서 setState()를 사용하는 것이다. setState()는 상태가 변경될 때 호출되며, Flutter는 이에 맞추어 UI를 다시 렌더링한다.
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
상태 관리: Provider 패턴
Provider는 Flutter에서 권장하는 상태 관리 방식 중 하나이다. Provider 패턴은 InheritedWidget을 기반으로 하여, 애플리케이션의 상태를 효율적으로 관리하고 하위 위젯에 데이터를 전달할 수 있다. Provider를 사용하면 애플리케이션의 상태를 전역에서 공유하거나, 특정 트리 구조에서만 상태를 공유할 수 있다.
Provider 사용 예시
- ChangeNotifier를 사용하여 상태를 관리할 수 있다. ChangeNotifier는 상태가 변경될 때 리스너에게 알리는 기능을 제공한다.
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 상태가 변경되었음을 알림
}
}
- Provider를 사용하여 ChangeNotifier를 하위 위젯에 제공할 수 있다.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
- Consumer 위젯을 사용하여 Provider에서 제공하는 데이터를 화면에 출력할 수 있다.
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(
child: Consumer<Counter>(
builder: (context, counter, child) {
return Text('Counter: ${counter.count}');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<Counter>().increment(),
child: Icon(Icons.add),
),
);
}
}
이 예시에서 ChangeNotifierProvider를 통해 Counter 객체를 위젯 트리 전체에 제공하고 있으며, Consumer 위젯을 사용하여 Counter의 상태를 읽고 UI에 반영한다.
BLoC 패턴
BLoC(Business Logic Component)는 Flutter에서 비즈니스 로직과 UI를 분리하는 패턴 중 하나이다. BLoC 패턴은 Stream을 사용하여 비동기적으로 상태를 관리하며, UI와 상태 로직을 명확하게 구분할 수 있게 해준다. 이 패턴은 특히 복잡한 애플리케이션에서 자주 사용된다.
BLoC 패턴의 구성
- BLoC 클래스: 비즈니스 로직을 처리하고 Stream을 통해 데이터를 제공하는 클래스이다.
- StreamController: Stream을 생성하고 데이터를 입력하는 컨트롤러이다.
- StreamBuilder: Stream에서 전달된 데이터를 기반으로 UI를 업데이트하는 Flutter 위젯이다.
BLoC 패턴 예시
class CounterBloc {
int _counter = 0;
final _counterController = StreamController<int>();
Stream<int> get counterStream => _counterController.stream;
void increment() {
_counter++;
_counterController.sink.add(_counter);
}
void dispose() {
_counterController.close();
}
}
CounterBloc 클래스는 내부적으로 StreamController를 사용하여 상태를 관리하며, increment 메서드를 통해 카운터를 증가시키고 Stream에 새로운 값을 제공한다.
UI에서는 StreamBuilder를 사용하여 Stream의 데이터를 받아 화면을 업데이트한다.
class MyHomePage extends StatelessWidget {
final CounterBloc _bloc = CounterBloc();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BLoC Example')),
body: Center(
child: StreamBuilder<int>(
stream: _bloc.counterStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return Text('Counter: ${snapshot.data}');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _bloc.increment,
child: Icon(Icons.add),
),
);
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
이 예시에서 StreamBuilder는 CounterBloc에서 제공하는 Stream을 구독하여, 새로운 데이터가 전송될 때마다 UI를 업데이트한다. BLoC 패턴을 사용하면 비즈니스 로직을 보다 명확하게 분리할 수 있으며, 재사용 가능한 코드를 작성하는 데 도움이 된다.
Riverpod 패턴
Riverpod는 Flutter에서 최근에 각광받고 있는 상태 관리 패턴이다. Provider 패턴을 발전시킨 형태로, 더 나은 성능과 오류 처리 기능을 제공한다. Riverpod의 특징은 상태를 전역으로 관리할 수 있고, 종속성 주입을 통한 테스트 및 유지보수성 향상이다.
Riverpod의 구성
- Provider: 상태나 객체를 제공하는 기본 단위이다.
- ConsumerWidget: Provider에서 제공하는 상태를 읽고 UI를 업데이트하는 위젯이다.
- ProviderContainer: 테스트 환경에서 Provider를 주입하는 역할을 한다.
Riverpod 예시
final counterProvider = StateProvider((ref) => 0);
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Example')),
body: Center(
child: Text('Counter: $counter'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.state).state++;
},
child: Icon(Icons.add),
),
);
}
}
이 예시에서 StateProvider는 상태를 제공하고, ConsumerWidget은 상태를 구독하여 UI를 업데이트한다. Riverpod는 Provider 패턴과 달리, 더 안전하고 예측 가능한 상태 관리 방식을 제공한다.