[Flutter] flutter 상태 관리

만들면서 배우는 플러터 앱 프로그래밍
최재원's avatar
Jul 30, 2025
[Flutter] flutter 상태 관리

1. 상태(객체의 상태)

  • 모든 객체는 상태를 가질 수 있다
class HomePage extends StatelessWidget { int num = 1; @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }
상태 변경 기본 공부 코드
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp(home: HomePage()); } } class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // 1. 상태 int num = 1; // 2. 행위 void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { print("rebuild 됨"); return Scaffold( appBar: AppBar(), body: Center(child: Text("${num}", style: TextStyle(fontSize: 50))), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: increase, ), ); } }

2. 불변 상태, 변하는 상태

Stateless

class HomePage extends StatelessWidget { int num = 1; @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }

Stateful

class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }

3. 상태가 변할 때 그림이 reload 될 때(stateless, stateful)

  • 상태가 있어도 불변이면 상태가 없는 것으로 친다
  • 상태가 변할 때 상태가 있는 것으로 친다
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp(); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 1. Page (화면 - Scaffold 가져야 한다) // 2. 클래스 (오브젝트 - 상태가 있다) // 3. StatelessWidget -> InMutableWidget(불변위젯) // -> 자식의 상태를 관리 하지 않는다 class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); // 내부적으로 자동으로 createState를 실행한다 } class _HomePageState extends State<HomePage> { // 1. 상태 int num = 1; // 2. 행위 void increase() { num += 1; print("num : $num"); setState(() {}); // 다시 _HomePageState를 new 해서 build를 새로 한는 것 } @override Widget build(BuildContext context) { print("빌드됨! : $num"); return Scaffold( body: Center(child: Text("$num", style: TextStyle(fontSize: 30))), floatingActionButton: FloatingActionButton( onPressed: increase, child: Icon(Icons.add), ), ); } }
notion image
notion image

4. context

  • 화면 그 자체, 화면 데이터를 말함
  • 도화지
  • 각 위젯이 가지는 것
notion image
컨텍스트 분리 x
  • StatefulWidget 으로 상태를 관리함
  • setState() 를 실행해서 다시 build 함
  • 상태 변경을 하면 HomePage 전체가 다시 그려짐
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ Expanded( child: Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ), ), Expanded( child: Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { num++; setState(() {}); }, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ), ), ], ), ), ); } }
 
컨텍스트 분리o 상태 변경x
  • 위젯을 분리 했으나 상태 변경을 할 수 없음
  • 상태를 가지는 위젯과 상태를 변경하는 위젯이 분리되어 사용이 불가능함
  • 그림을 다시 그릴 수 없음
  • 상태를 변경하고 싶으면 상태를 가지는 쪽에 상태를 변경하는 방법이 있어야 함
  • 디자인이 망가짐
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ Expanded( child: Header(), ), Expanded( child: Bottom(), ), ], ), ), ); } } class Header extends StatefulWidget { @override State<Header> createState() => _HeaderState(); } class _HeaderState extends State<Header> { // 1. 상태 int num = 1; @override Widget build(BuildContext context) { return Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ); } } class Bottom extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () {}, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ); } }
 
컨텍스트 분리o 상태 변경o 비용x(부모가 다시 그려짐)
  • 위젯을 분리했으나 상태 변경을 하려면 부모가 상태 관리를 하고 자식들에게 전달하는 방법을 사용해야 함
  • 상태 관리를 하면서 위젯을 분리하는데 성공 함
  • 그러나 결국 부모가 다시 build 하기 때문에 비용이 낭비됨
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Expanded(child: HeaderPage(num)), Expanded(child: MiddlePage(1)), Expanded(child: BottomPage(increase)), ], ), ), ); } } class HeaderPage extends StatelessWidget { int num; HeaderPage(this.num); @override Widget build(BuildContext context) { print("header"); return Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ); } } class BottomPage extends StatelessWidget { Function increase; BottomPage(this.increase); @override Widget build(BuildContext context) { print("bottom"); return Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { print("버튼 클릭됨"); increase(); }, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ); } } class MiddlePage extends StatelessWidget { final num; const MiddlePage(this.num); @override Widget build(BuildContext context) { return Container( color: Colors.white, ); } }
 
const 를 사용해 부분을 다시 build 하지 않게 한다
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { print("노란색"); return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Expanded(child: HeaderPage(num)), Expanded(child: const MiddlePage(1)), Expanded(child: const MiddlePage(1)), Expanded(child: BottomPage(increase)), ], ), ), ); } } class HeaderPage extends StatelessWidget { int num; HeaderPage(this.num); @override Widget build(BuildContext context) { print("header"); return Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ); } } class BottomPage extends StatelessWidget { Function increase; BottomPage(this.increase); @override Widget build(BuildContext context) { print("bottom"); return Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { print("버튼 클릭됨"); increase(); }, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ); } } class MiddlePage extends StatelessWidget { final int num; const MiddlePage(this.num); @override Widget build(BuildContext context) { print("middle $num"); print("middle ${this.hashCode}"); return Container( color: Colors.white, ); } }

5. const

코드
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { print("노란색"); return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Expanded(child: HeaderPage(num)), Expanded(child: const MiddlePage(1)), Expanded(child: const MiddlePage(1)), Expanded(child: BottomPage(increase)), ], ), ), ); } } class HeaderPage extends StatelessWidget { int num; HeaderPage(this.num); @override Widget build(BuildContext context) { print("header"); return Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ); } } class BottomPage extends StatelessWidget { Function increase; BottomPage(this.increase); @override Widget build(BuildContext context) { print("bottom"); return Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { print("버튼 클릭됨"); increase(); }, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ); } } class MiddlePage extends StatelessWidget { final int num; const MiddlePage(this.num); @override Widget build(BuildContext context) { print("middle $num"); print("middle ${this.hashCode}"); return Container( color: Colors.white, ); } }
  • 불변이다
  • 고정 디자인
  • 다시 build 할 때 const 인 위젯은 다시 그리지 않는다
  • 동일한 위젯이 존재하면 재사용함 → 플러터는 재사용하지 않고 다시 만듦
    • 동일한 위젯을 찾아서 재사용하는 프로그램도 있음
    • 플러터는 찾는 과정이 낭비라고 생각해서 새로 만드는 것 같음

사용 방법

@override Widget build(BuildContext context) { print("노란색"); return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Expanded(child: HeaderPage(num)), Expanded(child: const MiddlePage(1)), Expanded(child: const MiddlePage(1)), Expanded(child: BottomPage(increase)), ], ), ), ); }
class MiddlePage extends StatelessWidget { final int num; const MiddlePage(this.num); @override Widget build(BuildContext context) { print("middle $num"); print("middle ${this.hashCode}"); return Container( color: Colors.white, ); } }
  • 변수를 final 로 만들면 처음 초기화 되고 변하지 않는다

6. 위젯 트리와 비용

코드
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { print("홈페이지 빌드"); return Scaffold( body: Middle(), ); } } // 가짜 부모 << 혼자만 Stateful 이다 class Middle extends StatefulWidget { const Middle({ super.key, }); @override State<Middle> createState() => _MiddleState(); } class _MiddleState extends State<Middle> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: Top(num), ), Expanded( child: Bottom(increase), ), ], ); } } class Bottom extends StatelessWidget { Function increase; Bottom(this.increase); @override Widget build(BuildContext context) { return Center( child: Container( child: ElevatedButton( onPressed: () { increase(); }, child: Icon(Icons.add), ), ), ); } } class Top extends StatelessWidget { int num; Top(this.num); @override Widget build(BuildContext context) { return Center( child: Container( child: Text("$num", style: TextStyle(fontSize: 50)), ), ); } }
  • 페이크 트리를 하나 부모로 만들어서 사용

a 에 상태를 표시하고 c 에 상태 변경 행위를 넘겨주면(fake ❌)

  • HomePage 가 다시 rebuild 되면서 전체 그림이 다시 그려짐
  • 관계 없는 다른 위젯도 다시 그려짐
  • 자원 낭비
notion image

a 에 상태를 표시하고 c 에 상태 변경 행위를 넘겨주면(fake ⭕)

  • fake 부모가 다시 rebuild 되면서 fake, a, c 만 다시 rebuild 됨
  • 필요한 부분만 변경 가능함
  • 자원 낭비가 줄어듦
notion image

 
notion image
  • 다음과 같은 트리 구조도 있기 때문에 완벽하게 자원을 사용했다고 할 순 없다
  • 그래도 b 트리는 다시 그려지지 않기 때문에 그나마 낫다

7. 상태 관리를 외부에서 하는 법

notion image
  • 화면을 그리는 프로그램은 모두 상태 관리 라이브러리를 사용한다
  • 옵저버 패턴으로 만들어졌다
  • 구독자, 제공자(퍼블리셔)
  • 퍼블리셔는 상태 관리 라이브러리에게 한번만 요청하면 된다 (초록선)
  • 구독자는 상태 관리 라이브러리에게 계속 연결 되어있어야 한다 (파란선)
    • 연결 방법
      • read → 한번 값을 받고 끝
      • watch → 계속 값을 받을 수 있음
 
riverpod 세팅
import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1. 창고 데이터 타입(int 면 안 만들어도 됨) // 관리 타입은 1가지만 가능함 여러개면 클래스 타입 안에 넣어서 사용함 // 2. 창고 class HomeVM extends Notifier<int> { // 창고가 만들어질 때 초기화 메서드(return 하는 값을 창고가 state 로 관리함) @override int build() { return 1; } } // 3. 창고 관리자 final homeProvider = NotifierProvider<HomeVM, int>(() { return HomeVM(); });
 
Share article

jjack1