# UI 컴포넌트와 상태 관리

#### UI 컴포넌트 개요

Dart에서 UI 컴포넌트를 설계할 때는 **상태 관리**가 중요한 역할을 한다. 상태(state)란 UI가 보여주는 데이터의 현재 상태를 의미하며, 사용자의 입력, 네트워크 요청의 응답, 타이머와 같은 다양한 이벤트에 따라 변할 수 있다. 컴포넌트가 적절하게 상태를 유지하고 갱신해야만, 애플리케이션이 올바르게 작동한다.

Flutter에서 Dart 언어로 UI를 구성할 때, 모든 UI는 \*\*위젯(widget)\*\*으로 구성된다. 위젯은 Dart의 클래스이며, 화면에 표현될 시각적인 요소와 그 동작을 정의한다. UI는 상태에 따라 동적으로 변하는데, 이는 **상태 관리** 패턴을 통해 이루어진다.

#### 위젯 계층 구조

위젯은 트리 구조를 이루며, 이는 UI의 구성 요소 간의 관계를 나타낸다. 부모 위젯은 자식 위젯을 포함할 수 있으며, 자식 위젯의 상태에 따라 부모 위젯이 재구성될 수 있다. 위젯 계층 구조는 다음과 같은 흐름으로 이루어진다:

{% @mermaid/diagram content="graph LR
Root --> Parent1 --> Child1
Parent1 --> Child2
Root --> Parent2 --> Child3" %}

이와 같이 트리 구조로 구성된 위젯은 각자 자신의 상태를 관리할 수 있지만, 이때 상태가 변경되면 UI를 다시 그려야 하므로 상태 관리를 효율적으로 해야 한다.

#### 상태 관리의 종류

상태는 크게 두 가지로 나눌 수 있다:

1. **Stateless**: 변하지 않는 상태를 가진 UI.
2. **Stateful**: 변하는 상태를 가진 UI.

**Stateless Widget**

Stateless 위젯은 한 번 생성되면 내부 상태가 변경되지 않는 UI 컴포넌트이다. 단순한 텍스트나 이미지처럼 변하지 않는 UI 요소에 적합한다. Stateless Widget은 한 번만 빌드되고 이후 상태 변화에 대해 반응하지 않는다.

```dart
class MyStatelessWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('This is a stateless widget');
  }
}
```

**Stateful Widget**

Stateful 위젯은 상태가 변할 수 있는 UI 요소에 적합한다. 사용자의 입력이나 네트워크 응답에 따라 상태가 변하고, 이에 따라 UI도 업데이트된다. Stateful Widget은 상태 변화를 관리할 수 있는 **State 객체**와 함께 동작한다.

```dart
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: <Widget>[
        Text('Counter: $_counter'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

#### setState()와 상태 갱신

Stateful 위젯에서는 상태를 변경할 때 **setState()** 메소드를 호출한다. 이 메소드는 상태가 변경되었음을 Flutter에 알리고, 그에 따라 UI를 다시 빌드하게 된다. 그러나 setState()의 남용은 성능 저하를 초래할 수 있다. 그러므로 불필요한 상태 변화를 최소화하고, 꼭 필요한 경우에만 상태를 갱신해야 한다.

```dart
void _incrementCounter() {
  setState(() {
    _counter++;
  });
}
```

위 코드에서 `setState()` 안에 상태 변경 로직이 포함되어 있으며, 이 메소드를 호출하면 Flutter는 UI를 다시 빌드해 화면을 갱신한다.

#### 상태를 전달하는 방법

Flutter에서는 상태를 위젯 트리 전체에 전달해야 할 때가 많다. 이를 위한 몇 가지 기법이 있다:

* **상태를 상위 위젯에 저장**: 상위 위젯에 상태를 저장한 후 자식 위젯에게 전달할 수 있다. 이 방식은 위젯 간의 의존성을 최소화하지만, 트리가 깊어지면 상태 전달이 복잡해질 수 있다.

```dart
class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ChildWidget(counter: _counter, increment: _incrementCounter);
  }
}

class ChildWidget extends StatelessWidget {
  final int counter;
  final VoidCallback increment;

  ChildWidget({required this.counter, required this.increment});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('Counter: $counter'),
        ElevatedButton(
          onPressed: increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

이 방식은 단순한 구조에서는 효과적이지만, 복잡한 상태 전달에는 적합하지 않는다. 그래서 이를 개선하기 위한 다양한 패턴들이 존재한다.

#### InheritedWidget

**InheritedWidget**은 Flutter에서 상태를 자식 위젯에 전달하는 효율적인 방법이다. InheritedWidget은 위젯 트리에서 상위에 있는 위젯이 하위에 있는 모든 위젯에 데이터를 제공하는 데 사용된다.

```dart
class CounterProvider extends InheritedWidget {
  final int counter;
  final VoidCallback increment;

  CounterProvider({required Widget child, required this.counter, required this.increment})
      : super(child: child);

  static CounterProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterProvider>();
  }

  @override
  bool updateShouldNotify(CounterProvider oldWidget) {
    return oldWidget.counter != counter;
  }
}

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CounterProvider(
      counter: _counter,
      increment: _incrementCounter,
      child: ChildWidget(),
    );
  }
}

class ChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final provider = CounterProvider.of(context);

    return Column(
      children: <Widget>[
        Text('Counter: ${provider!.counter}'),
        ElevatedButton(
          onPressed: provider.increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

이 코드는 `InheritedWidget`을 사용하여 상태를 트리의 하위에 있는 모든 위젯에 효율적으로 전달한다. 이를 통해 위젯 트리 깊이에 상관없이 상태를 쉽게 공유할 수 있다.

#### Provider 패턴

**Provider 패턴**은 Flutter에서 더 널리 사용되는 상태 관리 패턴 중 하나로, `InheritedWidget`의 사용을 더 간단하게 만들어 준다. Provider는 상태를 전역적으로 관리하며, 다른 위젯에서 이를 쉽게 사용할 수 있도록 도와준다. Flutter 팀이 공식적으로 권장하는 패턴이기도 한다. Provider는 상태를 보다 효율적으로 트리 전체에 전달하고, 상태 변화를 감지하는 구조를 제공한다.

아래는 Provider 패턴을 사용하여 상태를 관리하는 예제이다:

1. **상태 클래스**를 정의하여 상태를 저장한다.

```dart
class CounterState with ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
}
```

2. **Provider**로 상태를 감싸서 위젯 트리 전체에서 사용할 수 있도록 한다.

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterState(),
      child: MaterialApp(
        home: CounterScreen(),
      ),
    );
  }
}
```

3. **Consumer**를 사용하여 상태를 접근하고 UI를 갱신한다.

```dart
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Consumer<CounterState>(
              builder: (context, counterState, child) {
                return Text(
                  '${counterState.counter}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterState>().increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
```

이 코드는 `ChangeNotifierProvider`를 사용하여 상태를 제공하고, `Consumer` 위젯을 통해 상태 변화를 감지하여 UI를 갱신하는 구조이다. 상태가 변경될 때마다 `notifyListeners()`가 호출되며, 이를 감지한 `Consumer`는 화면을 다시 그리게 된다.

#### 상태 관리에서의 성능 최적화

상태 관리의 핵심 중 하나는 성능 최적화이다. Flutter에서 상태를 자주 변경할 경우, 필요 이상의 위젯이 다시 그려지게 되면 성능 저하가 발생할 수 있다. 이를 막기 위해 몇 가지 기법을 사용할 수 있다.

**1. 적절한 상태 관리 범위 설정**

상태가 불필요하게 큰 범위에 전달되는 것을 막아야 한다. 위젯 트리의 깊은 곳에서만 필요한 상태는, 상위 위젯에서 관리하는 것보다는 하위 위젯에서만 전달되는 방식으로 설정하는 것이 좋다.

**2. `Consumer` 최적화**

`Consumer`는 상태가 변할 때마다 UI를 다시 그리지만, **하위 위젯 일부만 다시 그리는 구조**로 만들 수 있다. 예를 들어, 자식 위젯 중 일부만 상태 변경에 영향을 받는 경우, 해당 부분만 `Consumer`로 감싸서 재빌드를 최소화할 수 있다.

```dart
Consumer<CounterState>(
  builder: (context, counterState, child) {
    return Text('${counterState.counter}');
  },
)
```

**3. `Selector` 사용**

**Selector**는 상태의 특정 부분만을 선택하여 UI를 갱신하는 도구이다. 이를 통해 불필요한 재빌드를 막을 수 있다. 예를 들어, 상태의 여러 값 중 하나만 변경되었을 때 그 값에만 의존하는 위젯을 다시 빌드하도록 최적화할 수 있다.

```dart
Selector<CounterState, int>(
  selector: (context, counterState) => counterState.counter,
  builder: (context, counter, child) {
    return Text('$counter');
  },
)
```

위 코드에서는 `CounterState`의 `counter` 값이 변경될 때만 해당 위젯이 다시 빌드된다. `Selector`는 성능 최적화에 유용한 도구로, 꼭 필요한 경우에만 상태를 선택적으로 사용하게 해준다.

#### Flutter의 다른 상태 관리 기법들

Flutter에서 상태 관리를 위한 다양한 기법들이 존재한다. `Provider` 외에도 **BLoC**, **Redux**, **Riverpod** 등의 패턴과 라이브러리가 있다. 이러한 패턴들은 각각의 사용 목적에 맞게 적용될 수 있으며, 애플리케이션의 규모나 복잡성에 따라 적절한 선택이 필요하다.

* **BLoC**: 이벤트 기반 상태 관리를 위한 패턴으로, `Stream`을 통해 상태를 관리한다. 애플리케이션의 복잡한 상태 전환을 관리하는 데 적합한다.
* **Redux**: 전역 상태 관리 패턴으로, 애플리케이션의 상태를 단일 저장소로 관리한다. 상태 변화가 예측 가능하고 테스트 가능한 코드를 만들 수 있다.
* **Riverpod**: `Provider`의 개선된 버전으로, 더 유연한 상태 관리를 제공한다. 의존성 주입과 상태 관리를 통합적으로 처리할 수 있다.
