[플러터] 플러터 앱 상태 관리

플러터의 UI 변경 및 앱 상태 관리

플러터는 화면을 구성하는 UI 중 일부 UI의 데이터가 변경되었을 때 해당 UI를 수정하는 대신 전체 UI를 다시 빌드한다.

플러터는 선언적(declarative)으로 앱 상태(statement)를 관리한다. 애플리케이션 현재 상태를 인자를 UI 빌드 함수에 전달하면 새로운 UI가 빌드된다. UI를 구성하는 컴포넌트의 속성을 직접 변경할 필요 없이 선언적으로 앱의 상태를 변경함으로써 UI를 다시 그린다.

플러터의 프레임워크는 UI 뿐만 아니라 애니메이션 상태, 텍스쳐, 폰트 등과 관련된 앱 상태를 앱 실행 동안 메모리에 저장하여 관리한다.

앱 개발 시 직접적인 관리가 필요 없는 상태는 프레임워크가 처리하므로 상태는 “UI를 언제든 재구성하기 위해 필요한 모든 데이터”로 정의할 수 있다.

플러터 앱 개발 시 관리해야 하는 상태는 크게 두 가지 개념으로 나눌 수 있다.

  1. 임시(ephemeral) 상태 (UI 상태, 로컬 상태): 단일 위젯에 포함되는 상태
    • 위젯 트리의 다른 위젯들은 임시 상태에 접근할 필요가 없다.
    • 임시 상태는 직렬화가 필요 없고 복잡한 방식으로 변경되지 않는다.
    • 임시 상태는 StatefulWidget을 사용하여 관리하면 된다. ScopedModel이나 Redux와 같은 상태 관리 기술을 사용할 필요 없다.
    • setState() 메서드를 사용하여 StatefulWidgetState 클래스 내부 필드를 변경함으로써 임시 상태를 변경할 수 있으며 앱의 위젯 트리에서 다른 위젯들은 해당 필드에 접근할 필요가 없다. 즉, 해당 변수는 StatefulWidget 내부에서만 변경된다.
  2. 앱(app) 상태 (공유 상태): 앱의 다양한 부분에 걸쳐 공유하고자 하는 상태
    • 사용자 세션 간에 유지하고자 할 때 사용한다.


임시 상태와 앱 상태를 명확하게 구별하는 규칙이 존재하지는 않는다. State 클래스의 변수를 클래스 외부에서 변경할 수도 있다. 임시 상태였던 변수가 애플리케이션의 기능이 확장됨에 따라 앱 상태로 변경될 수도 있다.


앱 상태 관리 방법

  1. Provider링크 사용
  2. BLoC링크 사용
  3. Redux링크 사용


위젯 변경

플러터에서는 위젯 외부에서 메서드 호출을 통해 새로운 데이터를 적용하여 선언적으로 위젯을 변경시키는 대신, 위젯이 변경될 때마다 위젯을 새로 구성한다. 예를 들어, MyWidget.updateWith(somethingNew)와 같이 메서드를 호출하는 대신 MyWidget(contents)와 같이 생성자를 호출한다.

위젯 트리에서 위젯의 부모 위젯의 빌드 메서드에서만 새로운 자식 위젯을 생성(자식 위젯을 새로운 위젯으로 대체)할 수 있기 때문에 새로운 데이터는 위젯의 부모 또는 그 상위 위젯에 존재해야 한다. 위젯은 새로운 데이터를 어떻게 보여줄지에 대해서만 관심이 있으며 앱의 라이프사이클에 대해 신경쓰지 않아도 된다.

위젯이 변경되면 기존의 위젯은 새로운 위젯으로 교체된다. 변경되지 않고 교체되는 위젯의 특성에 따라 위젯을 불변(immutable)하다고 할 수 있다.

위젯 트리에서 서로 직접적인 부모 또는 자식 관계가 아닌 위젯의 경우 자식 위젯의 데이터 변경이 부모 위젯에도 전달되도록 하기 위해서는 변경된 데이터를 인수로 받는 콜백 메서드의 참조를 하위 위젯 생성자에 전달하면 된다. 하지만 이 방법을 사용할 경우 여러 곳에서 수정해야 하는 앱 상태가 있다면 많은 수의 콜백 메서드를 전달해야 한다.

플러터는 위젯에 데이터를 제공하고 해당 위젯의 자손 위젯(자식 뿐만 아니라 모든 하위 위젯)에 다시 전달하는 메커니즘을 특별한 위젯을 대상으로 제공하고 있다. InheritedWidget, InheritedNotifier, InheritedModel와 같은 특별한 종류의 위젯이 이에 해당한다. 이러한 저수준의 위젯을 다루는 대신 보다 사용하기 쉬운 provider 패키지를 사용함으로써 이러한 메커니즘을 통해 위젯의 상태에 접근할 수 있다.


Provider

provider 패키지를 사용하면 콜백 메서드 또는 InheritedWidgets에 대해 걱정할 필요가 없다. provider 패키지는 InheritedWidget 위젯을 사용하기 쉽고 재사용할 수 있도록 InheritedWidget를 래핑하여 제공함으로써 위젯이 데이터의 변경 사항을 감지할 수 있는 방법을 제공한다. provider 패키지의 ChangeNotifier, ChangeNotifierProvider, Consumer 세 클래스는 앱 상태의 변경 및 감지를 위한 기능을 제공한다.

ChangeNotifier는 앱 상태 변경을 알리는 역할을 하며, 앱 상태 변경의 알림을 감지 및 구독하는 리스너인 Consumer가 앱의 UI를 변경시킨다. ChangeNotifierProvider는 앱 상태 변경을 알리는 역할을 하는 ChangeNotifier의 인스턴스를 자손 위젯에 제공하는 역할을 한다. ChangeNotifierProvider를 통해 제공되는 ChangeNotifier가 앱 상태 변경을 알리면, Consumer는 변경을 감지 및 구독하고 변경된 데이터를 사용하여 UI를 변경하는 역할을 한다.


ChangeNotifier

플러터 SDK에 포함되어 있는, 단순한 클래스인 ChangeNotifier는 변경 알림을 리스너인 Consumer에게 제공하는 역할을 한다. 즉, 어떤 클래스가 ChangeNotifier를 상속하고 있다면, 해당 클래스의 변경 사항을 구독할 수 있다. ChangeNotifier는 앱 상태를 캡슐화하는 한 방법이다. 매우 단순한 앱에서는 하나의 ChangeNotifier로 해결할 수 있지만, 복잡한 앱의 경우 여러 모델이 존재하므로 여러 ChangeNotifier를 사용해야 한다. ChangeNotifierprovider와 함께 사용해야할 필요는 없지만 이 클래스를 사용하면 작업하기가 쉬워진다.

ChangeNotifier를 상속하는 모델 클래스에서는 앱 상태 변경을 알리기 위해 notifyListeners()라는 메서드를 호출하면 된다. 변경을 알리면 변경을 구독하는 리스너가 변경된 데이터(모델)를 사용하여 UI를 변경한다. 언제든지 이 메서드를 호출함으로써 모델의 변경이 앱의 UI를 변경하도록 만들 수 있다. notifyListeners() 메서드를 호출하는 곳 외에는 모델 구성 및 비즈니스 로직과 관련된 코드가 존재한다.

ChangeNotifier 클래스는 flutter:foundation의 일부이고 플러터의 고수준 클래스에 대한 의존성이 없어 테스트가 용이히다.


ChangeNotifierProvider

ChangeNotifierProvider는 앱 상태 변경을 알리는 역할을 하는 ChangeNotifier 인스턴스를 자손 위젯에 제공하는 역할을 한다. provider 패키지에 포함되어 있다.

ChangeNotifierProvider는 위젯 트리 상 앱 상태 변경 과정에서 하위 위젯들이 접근할 수 있는 상위 위젯에 위치시켜야 하지만 필요 이상으로 상위에 위치시킬 필요는 없다. 하위 위젯의 상태 변경이 영향을 미쳐야만 하는 상위 위젯들의 공통 상위 위젯에 위치시키면 된다.

ChangeNotiferProvider 생성자 호출 시 모델의 ChangeNotiferProvider 인스턴스를 새로 생성하는 빌더를 정의한다. ChangeNotifierProvider는 절대적으로 필요한 경우가 아니면 모델을 다시 빌드하지 않을 만큼 충분히 똑똑하며 모델 인스턴스가 필요하지 않을 때는 모델 클래스에서 자동으로 dispose() 메서드를 호출한다.

하나 이상의 ChangeNotifierProvider를 제공하기 위해서는 MultiProvider를 사용한다.


Consumer

ConsumerChangeNotifierProvider를 통해 제공되는 ChangeNotifier가 앱 상태 변경을 알리면, 변경을 감지 및 구독하고 변경된 데이터를 사용하여 UI를 변경하는 역할을 한다.

ChangeNotifierProvider를 통해 앱의 위젯에 변경 가능한 모델이 제공되면 이를 사용하면 된다. Consumer 위젯은 이 모델 인스턴스를 사용하여 변경된 데이터가 UI를 변경하도록 한다.

Consumer의 생성자 호출 시 제네릭을 통해 모델의 타입을 지정해야 하며, 지정하지 않을 시 provider 패키지는 관련된 기능을 제공할 수 없다. provider는 타입 기반으로 동작한다.

Consumer 위젯의 필수 인자는 빌더이며, 빌더 함수는 ChangeNotifier가 변경될 때마다 호출된다. 즉, 모델의 notifyListeners()를 호출할 때마다 해당하는 모든 Consumer 위젯의 모든 빌더 메서드가 호출된다.

빌더 함수는 세 가지 인자를 전달받는다.

  1. context: 모든 빌드 메서드에서 얻을 수 있다.
  2. ChangeNotifier 인스턴스: 모델에 존재하는 데이터를 사용하여 특정 시점에 보여질 UI를 정의할 수 있다.
  3. child: 최적화를 위해 존재하는 인자로, 모델이 변경될 때 따라서 변경되지 않는 큰 위젯 하위 트리가 Consumer 위젯 아래에 있는 경우 해당 위젯 트리를 한 번 구성한 후에는 빌더를 통해 가져올 수 있다. 즉, Consumer 위젯의 child 속성에 위젯을 구성하는 메서드를 지정하고, child 속성을 빌더 메서드의 인자로 전달한다.


Consumer 위젯은 가능한 위젯 트리의 깊은 곳에 위치시키는 것이 가장 좋다. 어딘가에서 위젯의 세부 사항이 변경되었다고 해서 UI의 많은 부분을 다시 빌드하고 싶지는 않을 것이기 때문이다.


Provider.of

Consumer 위젯에서 ChangeNotifier를 상속하는 모델 인스턴스의 변경 사항을 감지하고 데이터에 접근하여 변경 사항을 위젯에 반영하기 위해 Providerof() 정적 메서드를 사용할 수 있다. of() 메서드는 위젯 트리에서 가장 가까운 Provider<T>를 얻고 값을 반환한다. of() 메서드는 모델 인스턴스의 데이터에 접근하고 변경된 데이터를 위젯 빌드에 사용할 수 있게 해준다. of() 메서드 대신 provider 패키지가 제공하는 watch 메서드를 사용할 수도 있다.

때로는 UI를 변경하기 위해 모델의 데이터가 실제로 필요하지 않지만 여전히 해당 모델에 접근이 필요한 경우가 있다. 이 경우 Consumer 위젯을 사용할 수는 있지만 다시 빌드할 필요가 없는 위젯을 다시 빌드하도록 프레임워크에 요청하게 되면 리소스가 낭비된다. 이 경우 빌드 메서드에서 Provider.oflisten: false 매개변수와 함께 사용하여 notifyListeners() 메서드가 호출될 때 해당 위젯이 다시 빌드되지 않도록 할 수 있다.

listen 값이 true라면, 데이터 변경은 위젯에 대해서는 State.build() 메서드, StatefulWidget에 대해서는 State.didChangeDependencies() 메서드를 새롭게 트리거한다. State.initState() 내부의 Provider.of()를 호출하거나 Providercreate() 콜백 메서드를 호출할 수 있도록 하기 위해서는 listen 값을 false로 설정해야 한다.


Provider를 사용한 코드 작성

데이터 조회 및 위젯 변경

class Model extends ChangeNotifier {
  // 모델이 가지는 데이터를 정의한다. 외부에서 접근 불가능한 캡슐화된 상태이다.
  final String _data = '';
  final String get data => data;

  // 데이터를 변경한다. 
  void changeWidget() {
    data = await getData();
    // 이 모델의 변경 사항(데이터의 추가)을 구독하는 위젯(Consumer)에게 다시 빌드하도록 요청한다.
    notifyListeners();
  }
}

...

Widget getWidget(BuildContext context) {
  var model = Provider.of<Model>(context);
  return Text(model.data);
}


ChangeNotifier를 사용하여 데이터 모델의 변경을 감지하고 데이터 모델을 변경하는 별도의 이벤트가 존재하는 경우, 데이터 모델을 직접 생성자로 인스턴스화한 변수를 참조하여 해당 이벤트 발생 시 모델을 변경하는 메서드를 호출하면 안 된다. 대신 Provider.of를 사용하여 해당 모델에 대한 참조만 가져온 후 데이터 모델을 변경하는 메서드가 호출되면 Consumer 위젯의 빌더가 해당 데이터 모델의 변경을 감지하고 위젯을 재빌드하도록 만든다.


FutureBuilder

위젯의 build() 메서드 내에 FutureBuilder를 사용하여 비동기 호출을 수행하는 경우 비동기로 호출할 메서드를 직접 작성하면 안 된다. 플러터에서 UI가 갱신될 때마다 위젯의 build() 메서드가 호출되기 때문에 해당 비동기 호출이 매번 호출된다. 예를 들어, 소프트 키보드를 열고 닫거나, 패널(panel) 형태의 위젯을 열고 닫을 때마다 build() 메서드가 호출된다. 따라서 적절한 비동기 호출을 위해서는 위젯에서 사용할 비동기 호출을 수행하고 결과를 저장할 Future를 선언 및 초기화하고, FutureBuilderfuture 파라미터에 해당 Future 타입의 변수를 할당하는 것이 필요하다. ```dart class MyStatefulWidget extends StatefulWidget { … }

class _MyState extends State { late Future myFuture;

@override void initState() { myFuture = widget.getDataAsync(); super.initState(); }

@override Widget build(BuildContext context) { … child: FutureBuilder( future: myFuture, ) } } ``


참고

Comments