Flutter

[Flutter] 상태 관리

_hong 2025. 1. 22. 17:42

상태 관리

 

플러터 공식 문서에 따르면 Flutter 앱에서 상태란, 앱이 UI를 표시하거나 시스템 리소스를 관리하기 위해 사용하는 모든 object를 의미한다. 즉, 상태 관리란 이러한 상태에 가장 효율적으로 접근하고, 이를 서로 다른 위젯들 간에 공유할 수 있도록 앱을 구성하는 방법을 말한다.

 

그러니까 Flutter에서 State(상태)는 시간에 따라 변경될 수 있고, UI의 모양 또는 동작에 영향을 미치는 정보를 의미한다. Flutter 앱에서 상태의 예로는 다음과 같은 것들이 포함될 수 있다.

  • 사용자 입력
  • 로그인 정보
  • 네트워크 데이터
  • 기기의 화면 방향
  • 기타 변화 가능한 정보들

앱의 상태가 변경되면(예: 사용자가 설정 화면에서 스위치를 켤 때) 상태를 변경하고, 그러면 사용자 인터페이스가 다시 그려진다.  

 

Flutter에서는 UI가 일반적으로 위젯으로 나뉘며, 상태 관리는 이러한 위젯들 간에 데이터가 공유되고 업데이트되는 방식에 초점을 맞춘다. 좋은 상태 관리는 앱의 한 부분에서 변화가 생겼을 때, 해당 변화가 UI의 다른 관련 부분에도 정확하게 반영되도록 한다.

 

상태 관리 왜 중요할까? 

 

전 두 가지 문제가 있다고 생각합니다. 첫 번째는 유지 보수, 두 번제는 최적화 문제입니다. 하나의 위젯만을 가지는 앱은 위젯 안에서 상태를 관리하게 됩니다. 하지만 여러 위젯들로 이루어진 위젯트리를 만들고 상태를 공유하는 경우엔 문제가 나타나기 시작합니다.

 

여러 위젯 간에 상태를 수동으로 전달하려고 하면, 특정 데이터가 어디에서 생성되고 어디서 변경하고, 사용되는지 파악하기가 어려워집니다. 특히, 상태를 전달받는 중간 위젯들은 그 데이터와 강하게 묶이게 되니까 재사용하기도 힘들어지기도 합니다.

게다가, 상태를 공유하는 위젯들이 많아질수록 상태 변화가 생길 때, 필요 없는 위젯들까지 다시 렌더링 되는 경우가 생길 수도 있어요. 이러면 성능에도 영향을 주고, 코드 관리도 점점 복잡해집니다. 


위젯의 라이프 사이클

 

Flutter에서는 "everything is a widget"라는 말을 자주 듣게 된다. 위젯은 Flutter 앱의 사용자 인터페이스를 구성하는 기본 단위이며, 각각의 위젯은 불변(immutable)이다.

 

Stateless Widget 

 

공식 문서에 따르면, Stateless 위젯은 자체적으로 상태를 가지지 않고, 고정된 데이터 또는 부모로부터 전달받은 데이터를 기반으로 UI를 구성하는 역할을 한다.

 

  • Stateless 위젯은 한 번 빌드된 이후에는 상태를 변경하거나 수정할 수 없는 위젯을 의미합니다.
  • 이러한 위젯은 빌드된 이후 불변(immutable) 상태를 유지합니다.
  • 데이터, 위젯, 아이콘, 변수 등의 변경 사항이 발생해도 앱이나 UI의 상태에는 영향을 미치지 않습니다.
  • Stateless 위젯은 단순히 build() 메서드를 오버라이드하고, 그 메서드에서 하나의 위젯을 반환합니다.

Stateful Widget

 

StatefulWidget은 실시간으로 상태를 변경할 수 있는 위젯입니다. 내부 상태나 외부 시스템 상태의 변화에 따라 UI가 동적으로 변경될 때 적합하다.

 

Stateful Widget Lifecycle

 

createState(): Stateful Widget 을 생성할 때, state object를 생성하고 반환하기 위해 호출됩니다.

 

mounted: bool 값이며, buildContext가 위젯에 할당되면 true가 됩니다.

 

initState(): 위젯이 빌드되기 직전에 호출되는 메서드입니다. 이 메서드 내부에서 빌드 메서드에 필요한 변수를 초기화할 수 있습니다. State 객체가 여전히 위젯 트리에 연결되어 있는지를 확인하는 변수입니다. mounted가 true일 때는 해당 State 객체가 여전히 화면에 렌더링 되고 있는 상태이며, false일 때는 이미 위젯 트리에서 제거된 상태입니다. 

 

didChangeDependencies(): initState() 메서드 바로 뒤에 호출되고, 상태 객체의 종속성이 변경될 때 호출됩니다. didChangeDependencies는 위젯이 처음 생성되었을 때와, InheritedWidget과 같은 의존성의 변화가 있을 때 호출됩니다.

 

build(): 이 메서드는 화면에 UI를 표시합니다. 위젯을 반환합니다. initState() 메서드 다음에 호출됩니다. 이 메서드는 setState가 호출되면 다시 빌드됩니다.

 

didUpdateWidget(): 부모 위젯에 의해 rebuild가 필요한 경우 build() 함수 호출 전에 호출된다. 부모 위젯의 변경으로 자식 위젯이 위젯 속성 값들의 변경이 생기고, 해당 위젯이 다시 빌드되어야할때 호출됩니다.

 

setState(): UI를 업데이트해야 할 때 호출되는 메서드입니다. build 메서드 바로 다음에 호출할 수 없습니다. 이 메서드가 호출되면 UI가 build 메서드를 다시 호출하여 다시 빌드됩니다.

 

deactivate(): 위젯이 팝업될 때 호출되지만 현재 프레임 변경이 완료되기 전에 다시 삽입될 수 있습니다.

 

dispose(): 이 메서드는 상태 객체가 완전히 제거된 후 호출되거나 화면이 팝업 될 때 호출됩니다.

 

setState 단점

 

Widget은 기본적으로 트리로 이루어져있습니다. 그리고 setState는 상태 변경이 생겼을 때 위젯 트리 부모의 하위 위젯들을 모두 리빌드 합니다. 만약 위젯 트리의 depth가 깊을 때 불필요하게 다시 렌더링 되어 UI 반응 속도가 저하될 수 있습니다.

 

이를 해결해줄수 있는 플러터의 상태 관리 도구들이 존재한다.


GetX

GetX의 홈페이지에선 아래와 같이 GetX의 핵심 원칙을 설명한다.

 

GetX는 Flutter를 위한 가볍고 강력한 솔루션으로, 다음과 같은 기능을 제공합니다.
고성능 상태 관리
의존성 주입
라우트 관리

 

GetX의 3가지 핵심 원칙

 

1. 생산성

- GetX는 쉬운 문법을 제공하여 개발 시간을 단축하고 앱의 최대 성능을 이끌어냄

- context 없이 상태에 접근 가능위젯 트리에 의존하지 않음 

 

2. 성능

- GetX는 최소한의 리소스 사용을 목표로 설계됨

- Stream이나 ChangeNotifier를 사용하지 않음 → 가볍고 빠름

- 기본적으로 사용되지 않는 리소스는 자동으로 메모리에서 제거됨

- 특정 리소스를 유지하려면 permanent: true를 설정해야 함

- 지연 로딩 가능 -> 필요할때만 생성 가능

- Ex) Get.lazyPut(() => CounterController());

 

3. 구조화

- 뷰, 프레젠테이션 로직, 비즈니스 로직, 의존성 주입, 네비게이션을 완전히 분리

- context 없이 라우트 이동 가능 → 위젯 트리에 종속되지 않음

- InheritedWidget 통해 컨트롤러(Controller) BLoC 접근할 필요가 없으므로, 프레젠테이션 로직과 비즈니스 로직을 시각적 계층()에서 완전히 분리할 있다.

1. BuildContext를 사용하지 않는다?

GetX는 BuildContext를 사용하지 않고 상태 또는 라우트를 관리하여, 위젯트리에 접근하지 않는다. 다른 상태 관리 도구들은 BuildContext를 사용하여 상태를 가져오기 때문에 위젯 트리 내에서 상태를 전달하거나, 또한 자식 위젯에게 context를 계속 전달해야 하는 경우가 있다. 

 

GetX의 상태 관리

class CounterPage extends StatelessWidget {
  final CounterController controller = Get.put(CounterController()); // DI(의존성 주입)

  @override

  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Obx(() => Text('${controller.count}')), // context 없이 상태 접근 가능
          ElevatedButton(
            onPressed: controller.increment, // context 없이 컨트롤러 접근
            child: Text("증가"),
          ),
        ],
      ),
    );
  }
}

 

GetX의 라우트 관리

// 일반적인 Navigator 방식 (context 필요) 

Navigator.push( context, MaterialPageRoute(builder: (context) => NextPage()), ); 

// GetX 방식 (context 필요 없음

Get.to(NextPage());

 

 

그럼 Provider는 BuildContext를 어떻게 사용할까? 단점이 뭘까?

 

Provider는 BuildContext에 의존하여 위젯 트리 구조가 변경될 때 Context 관련 코드도 수정해야 한다.

Provider를 통해 counter를 가져오는 경우를 살펴보자.

 

 runApp(
    ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: LoginPage(),
    ),
  );

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<CounterProvider>(context);

    return Column(
      children: [
        Text('${counter.count}'),
        ElevatedButton(
          onPressed: counter.increment,
          child: Text("증가"),
        ),
      ],
    );
  }
}

 

CounterPage가 LoginPage 내부에 있다고 가정했을 때, Provider.of <CounterProvider>(context)가 ChangeNotifierProvider를 정상적으로 찾을 수 있다.

 

하지만 CounterPage를 HomePage 내부로 이동시킨다면, context는 HomePage의 context를 따르게 된다. 정상적으로 ChangeNotifierProvider를 찾을 수 없고 오류가 발생할 것이다.

 

class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Text("홈 화면"),
          CounterPage(),
        ],
      ),
    );
  }
}

 

 

Context에 의존하는 Provider는 위젯 트리의 구조를 변경할 때 수정되어야 하는 코드가 많아질 수 있는 단점이 존재한다.

 

2. Stream이나 ChangeNotifier를 사용하지 않아서 가볍고 빠르다?

 

Stream은 일반적인 상태 관리에서는 불필요한 리소스 낭비가 발생할 수 있다. 예를 들어, 값이 한 번만 바뀌어도 계속해서 Stream을 유지해야 하므로 메모리 사용이 증가할 수 있다.

 

ChangeNotifier는 내부적으로 리스너(콜백 함수)를 리스트에 저장하고 관리한다. 또한 상태 변화를 알려주는 함수인 notifyListeners()이 호출되면 ChangeNotifier를 구독하는 모든 위젯이 rebuild 된다. 구독 범위가 크면 불필요한 위젯까지 리빌드 되어 비효율적 일수 있기 때문이다.

 

3. 뷰, 프레젠테이션 로직, 비즈니스 로직, 의존성 주입, 네비게이션을 완전히 분리가 가능하다?

 

완전히 로직 간 분리가 가능하다는 말이 이해가 가지 않았다. 다른 상태 관리 도구는 뷰와 프레젠테이션 로직 간에 어떻게 의존생이 생기는 건가?, getX 없이도 분리가 가능하지 않을까?..  의문이 들었다.

 

다른 상태 관리 도구들은 BuildContext를 사용하여 위젯 트리에서 위젯을 찾거나 상태를 구독한다. 또한, BuildContext를 이용해 라우팅을 관리하는 경우도 많다.

만약 UI에서 라우팅을 처리한다면, UI에서 BuildContext를 사용해 라우팅을 직접 제어하게 된다. 이 경우, UI 로직과 네비게이션 로직이 결합되며, UI가 상태 변화에 따라 네비게이션을 결정하게 된다.

반면, 라우팅이 컨트롤러 내에서 이루어진다면, 그 컨트롤러가 BuildContext에 의존하게 된다. 이 경우, 컨트롤러가 UI 로직에 의존하게 되어, UI와 비즈니스 로직 간의 결합을 완전히 제거하는 것이 어려워진다.

 

 

 

참고 문서