[Flutter] 상태관리의 필요성
상태관리(State Management)란?
먼저 상태(State)는 위젯이 활성화되고 데이터를 메모리에 저장한 것입니다. 쉽게 말해 상태는 데이터입니다. 아래 화면에서 필요한 데이터를 나열 해봅시다.
Flutter의 상태관리
Flutter와 같은 선언형 프레임워크는 UI를 변경하기 위해서 위젯을 다시 빌드 해야 합니다. 명령형 UI 처럼 외부에서 메서드를 호출해 위젯을 강제로 변경하는 것이 어렵습니다. 그렇다면 Flutter에서 어떻게 새로운 상태로 위젯을 업데이트 하는지 간단히 알아봅시다.
먼저 createState()를 호출해 상태를 별도의 State 클래스에 저장합니다. 상태를 수정해야 할 경우에 setState()를 호출하면 build()가 호출되면서 수정된 상태로 UI가 다시 그려집니다. 그리고 dispose()로 상태를 위젯트리에서 제거할 수 있습니다. 하지만 setState()만으로 상태관리를 하기엔 몇 가지 문제가 있습니다. 기본적으로 위젯은 트리 구조입니다. 따라서 어떤 위젯에서 setState()를 호출하면 그 위젯과 하위 위젯 전부 build()가 호출되어 UI가 렌더링 되기 때문에 불필요한 렌더링이 발생해 성능이 저하될 수도 있습니다.
Flutter 공식 문서의 한 예시를 통해 어떤 경우에 불필요한 렌더링이 발생하는지 알아봅시다. MyListItem의 각 위젯을 터치해 장바구니에 담고, MyCart에서 담은 목록을 확인하는 앱을 만든다고 합시다. 장바구니 상태를 업데이트해서 UI를 바꾸려면 위젯을 다시 빌드해야 하며, 트리 구조로 인해 이 상태는 바뀌는 위젯 또는 위젯의 상위에 있어야 합니다. 그래서 Cart 상태를 MyApp 위젯에서 관리하면 MyApp에서 setState()를 호출했을 때 Cart를 사용하지 않는 MyApp의 자식 위젯 MyAppBar도 불필요하게 렌더링되는 것을 아래 위젯트리 예시에서 알 수 있습니다.
그리고 어떤 위젯의 상태를 다른 위젯에서 사용해야 할 경우, 위젯의 생성자로 상태를 넘겨야 하는데 위젯트리가 복잡하고 깊어지면 데이터를 사용하지 않는 위젯도 거쳐야 하는 문제가 있습니다. 다음은 C 위젯에서 data를 사용하기 위해 B 위젯에서는 필요없는 상태를 전달하는 예시입니다. B 위젯에서는 data를 사용하지 않지만 자식 C 위젯에서 사용하기 위해 상태를 전달했습니다.
지금까지는 공식예제를 통해 소개해보았는데, 아래에서는 기본적으로 Flutter에 내장되어있는 ChangeNotifier를 통해 상태관리의 예시를 들어보겠습니다.
ChangeNotifier
ChangeNotifier는 플러터 SDK에 포함된 간단한 클래스로, 리스너에 변경 알림을 제공합니다. 즉, 무언가가 이면 ChangeNotifier변경 사항을 구독할 수 있습니다. (이 용어에 익숙한 사람들을 위해 말씀드리자면, Observable의 한 형태입니다.)
ChangeNotifier를 사용해 로그인 기능중에 아이디 저장/자동 로그인에 대한 상태를 생성해보겠습니다.
Model
class SignInModel {
bool isSaveId = false;
bool isAutoLogin = false;
}
model에서는 아이디 저장, 자동로그인에 대한 상태를 가지고 있습니다.
Controller(ChangeNotifier)
class SignInController with ChangeNotifier {
final model = SignInModel();
void toggleSaveId() async {
model.isSaveId = !model.isSaveId;
UserStorage.writeIsSaveId(model.isSaveId);
notifyListeners();
}
void toggleAutoLogin() async {
model.isAutoLogin = !model.isAutoLogin;
UserStorage.writeAutoLogin(model.isAutoLogin);
notifyListeners();
}
}
위 Controller에서는 아이디 저장, 자동 로그인 위젯을 클릭하면 생성되어있는 Model의 상태를 변경시키고, 해당 상태를 위젯에 알리는 notifyListener()를 호출하여 위젯에 알립니다.
View
final _controller = SignInController();
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Scaffold(
body: _buildPage(context),
);
},
);
}
}
ChangeNotifier를 AnimatedBuilder의 animation에 주입할 Controller를 생성하여 값을 할당해줍니다.
이렇게 생성된 controller에 접근하여 값에 접근, 변경이 가능합니다.
위 예시를 통해 라이브러리 없이 상태관리하는법도 소개해봤습니다.
하지만 위 내용 처럼 상태관리를 한다면 값을 계속 주입받게되고, 유연하게 상태를 관리할 수 없게됩니다.
Flutter에서는 GetX, BLoC, Provider, Riverpod 등 대표적인 상태관리 라이브러리가 있으며 각각의 장단점을 살펴보겠습니다.
GetX
- 아주 쉽고 생산성이 높아 인기가 많다.
- 상태관리 외에도 네비게이션, HTTP, Snackbar, Dialog 등 다양한 기능을 제공한다.
- BuildContext와 StatefulWidget을 쓸 필요가 없다.
- Flutter의 중요한 기본 객체나 라이프 사이클을 사용하지 않기 때문에 Flutter의 기본을 배우기엔 좋지 않다.
BLoC
- Business Logic Component
- 비즈니스 로직에서 UI를 분리하기 쉽다.(.dart 파일을 분명하게 분리 가능)
- Stream을 이용해야 한다면 강력한 라이브러리이다. (e.g. 채팅)
- UI에서 발생하는 이벤트를 감지한다.
- Stream을 이용하기 때문에 러닝커브가 있는 편이다.
Provider
- Flutter 공식 문서에서 사용하는 라이브러리이다.
- BuildContext를 활용하며 Flutter 자체를 최대한 활용하는 형태이다.
- Riverpod이 나왔기 때문에, 추후에 deprecated될 가능성이 있다.
Riverpod
- Provider의 단점을 개선한 라이브러리이다.
- Flutter에 의존하지 않는다. (Flutter 없이 Dart만으로도 사용이 가능)
- 컴파일타임 동안 안전하다.