1. 뭐라고하냐

Flutter에서 그림(UI)는 항상 데이터를 기반으로 그려진다. 이때 말하는 데이터는 오브젝트, 조금 더 자세하게 말하자면 오브젝트가 가진 객체의 상태를 의미한다. 모든 오브젝트는 상태를 가지며, 이 상태가 변할 수 있는지 여부가 중요한 기준이 된다. 상태는 있지만 변하지 않으면 불변이다. 예를 들어, 생성자에서 값이 정해지고 이후에 바뀌지 않는다면 그것은 불변 상태라고 할 수 있다. 반면 setter가 존재하여 외부에서 값을 바꿀 수 있다면 상태는 가변이다. 즉, 상태가 존재하는 것과 그 상태가 바뀔 수 있는지는 다른 문제이다. Flutter에서 중요한 것은 상태가 변하느냐이며, 상태가 변하면 그림도 다시 그려진다. 이런 구조 때문에 Flutter에서는 상태가 변하는지에 따라 Stateless와 Stateful로 나뉜다. 상태가 있지만 변하지 않으면 Stateless, 상태가 있고 변할 수 있으면 Stateful이라고 한다. 정리하자면, 모든 오브젝트는 상태를 가지고 있고, 그 상태가 변할 수 있느냐에 따라 그림을 다시 그릴지 말지가 결정된다. setter가 없으면 상태는 불변이고, 그림은 유지된다. setter가 있으면 상태가 변할 수 있고, 그림은 다시 그려진다. Flutter는 이런 식으로 상태를 기반으로 그림을 다시 그리는 구조를 가진다. 그림을 다시 그린다는 것은 곧 비용이 발생하는 작업이다. 그래서 상태를 잘 나누고, 변하지 않는 값들은 불변으로 유지하는 것이 중요하다.
2. 상태 관리 1 (1)
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> {
int num = 1;
@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: () {
num = num + 1;
setState(() {}); // rebuild
},
),
);
}
}
Build
위의 코드를 실행한 뒤 +모양의 버튼을 클릭하면 화면의 숫자가 증가하는데 console에 찍히는 print문을 통해 이는 버튼을 클릭한 만큼 화면이 rebuild 되어 숫자가 변경되는 것임을 알 수 있다. 이때 console에 찍히는 print문은 HomPage의 build 함수 안에서 Scaffold가 실행되기 직전의 자리에 위치한다. 즉 +모양의 버튼을 클릭하면 Scaffold 영역인 화면 전체가 전부 다시 그려지면서 화면의 숫자가 증가한다.
이때 build는 프레임워크에게 제어 권한이 있어서 개발자가 마음대로 빌드할 수 없는데 setState 함수를 사용하면 현재의 상태를 가지고 rebuild 할 수 있다.

행위
위의 코드에서 onPressed가 받는 익명함수가 num이라는 상태를 변경하는 행위이다. 이때 행위가 상태를 변경한다면 익명함수처럼 바로 그 위치에 작성해도 되지만 아래의 두 번째 사진처럼 함수를 상태의 아래 위치로 빼서 명확하게 써 두는 것이 좋다.


행위 전달 방식 3가지
방식 | 설명 | 문법 예시 | 특징 |
1. 익명 함수 (Anonymous Function) | 함수에 직접 동작을 작성 | onPressed: () { num++; } | - 가장 직관적- 짧은 로직에 적합- 재사용 불가 |
2. 익명 함수 내부에서 이름 있는 함수 호출 | 동작을 미리 정의해두고 호출 | void increase() { num++; }onPressed: () { increase(); } | - 로직 분리 가능- 가독성 향상- 재사용 가능 |
3. 함수 자체 전달 (함수 변수) | 함수 참조를 직접 전달 | onPressed: increase | - 가장 간결- 매개변수 없는 함수일 때만 가능- 콜백 전달에 유리 |
상태를 변경하기 위한 행위는 익명 함수로 바로 전달하는 것 보다는 상태의 아래쪽에 함수로 만들어서 전달하는 것이 낫다.
함수를 전달하는 3가지 방법 (1) - 익명함수

함수를 전달하는 3가지 방법 (2) - 이름이 있는 함수

함수를 전달하는 3가지 방법 (3) - 1. 매개변수가 있을 때 함수 자체를 전달

함수를 전달하는 3가지 방법 (3) - 2. 매개변수가 없을 때 함수 자체를 전달

setState()로 rebuild하기
현재 코드를 실행해보면 console에는 상태인 num이 행위인 increase 함수에 의해 변경된 것이 확인 가능하다. 그러나 화면은 상태가 변경된 것이 반영되지 않은 그림이다. UI, 즉 화면의 그림은 상태만 변경한다고 바뀌지 않고 setState함수를 통해 build함수를 재요청하여 rebuild해야 변경된다.

행위 increase 함수 안에
setState(() {});
를 추가한 다음 실행하면 console 뿐만 아니라 화면 또한 숫자가 증가하는 것을 확인할 수 있다.
Context
build 함수가 매개변수로 받는
BuildContext
는 위젯이 그려지는 위치나 구조와 같은 화면 정보를 담고 있는 객체이다. context는 그 화면의 정보를 담고 있기 때문에 이 화면의 정보를 그림을 그리는 flutter에게 전달하게 된다. 이때 context가 없으면 도화지가 없으니까 그림을 못 그리는 것과 같다. 따라서 context는 도화지이자 화면이다.만약에 아래의 코드에서 Center 부분의 그림만 다시 그리고 싶다면 Center를 위젯으로 분리하여 Center만의 화면, Center만의 context가 생기면 그 context만 새로 그리면 된다.

전체 코드
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++;
print("num : $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,
),
);
}
}
3. 상태 관리 1 (2)
직접 코드 작성해보기
Page는 화면이고 화면은 무조건 Scaffold를 들고 있다.
클래스는 오브젝트이고 오브젝트는 상태를 가지고 있을 수도 있고 없을 수도 있는데 상태가 없는 경우는 거의 없으므로 그냥 상태가 있다고 하자. HomePage가 상속받는 StatelessWidget은 위젯의 상태가 없다는 뜻인데 정확하게 말하자면 Immutable Widget, 불변 위젯이다. 예를 들어 HomePage 클래스 안에 int num이라는 상태가 있는데 이건 HomePage의 상태이고 부모인 StatelessWidget은 HomePage의 상태를 관리하지 않는다. 반대로 StatefulWidget은 StatelessWidget과 반대로 자식이 들고 있는 위젯을 관리한다.

Scaffold의 속성 중 하나인 floatingActionButton는 우측 하단에 떠있는 버튼이다. 이때 onPressed로 increase를 전달받아서 실행하는 것이 아니라 increase함수의 내부를 전달하는 것이다.

코드를 실행하면 console에서는 num 상태가 변하는 것을 볼 수 있지만 화면은 rebuild가 되지 않았기 때문에 여전히 1이다.

현재 코드에서 객체가 망가지지 않도록 build만 실행할 수 있으면 되는데 build는 프레임워크의 제어권에 있기 때문에 내 마음대로 build가 불가능하다. setState 함수를 통해 rebuild 할 수 있지만 StatelessWidget은 자식의 상태를 관리하지 않기 때문에 setState 함수를 제공하지 않는다. 따라서 StatelessWidget을 StatefulWidget으로 변경한다.

Flutter에서 위젯을 화면에 표시할 때, 해당 위젯이 StatelessWidget인지 StatefulWidget인지 먼저 확인한다. StatelessWidget은 상태가 없는 정적인 화면을 구성하기 때문에 단순히 build() 메서드를 호출하여 화면을 그린다. 반면 StatefulWidget은 상태를 가질 수 있는 위젯으로, 화면을 구성하기 전에 먼저 createState() 메서드를 콜백(함수 자체를 다름 함수나 위젯에 전달하여 전달받은 쪽이 나중에 필요할 때 직접 실행)한다. 이 메서드는 상태를 관리할 수 있는 State 객체를 반환하며, 이후에는 이 State 객체의 build() 메서드를 통해 화면을 그린다.
StatefulWidget은 자체적으로는 변하지 않고, 상태 변경과 화면 갱신은 분리된 State 클래스에서 담당한다. 그래서 StatefulWidget을 사용할 때는 반드시 두 개의 클래스를 정의해야 한다. 하나는 상태를 가지지 않는 껍데기 위젯인 StatefulWidget 클래스, 다른 하나는 실제 상태를 가지고 UI를 제어하는 State 클래스이다.
이 구조 덕분에 상태가 바뀌었을 때 setState()를 호출하면 Flutter는 해당 State 객체의 build()를 다시 호출하여 화면을 갱신합니다. (이미 생성된 State 객체의 build() 호출)



4. 상태 관리 2
전체 코드
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@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,
),
),
),
),
),
),
],
),
),
);
}
}
위의 HomePage Context는 노란색의 전체 도화지, Header Context는 빨간색 도화지, Bottom Context는 파란색 도화지 영역이다. 증가 버튼을 클릭하게 되면 빨간 도화지만 새로 그리는 것이 효율적이다. 그러나 위의 코드에서는 그렇게 동작하지 않는데 왜 안되는지 하나씩 알아보자

_HomePageState 클래스의 build 메서드 내부 return문 전에 print문으로 로그를 찍어보면 빨간색 영역인 Header가 아니라 HomePage인 전체 Container가 rebuild 된다는 것을 알 수 있다.

Header가 상태를 가지게 되면서 StatefulWidget이 되고, Bottom은 num이라는 생태는 가질 필요 없지만
행위는 bottom이 가져야 된다. 근데 num이라는 상태 가질 필요는 없음
bottom을 stateful로 변경하면 onpress에 setState 가능 근데 그렇게 되면 bottom자기자신이 rebuild됨
=근데 내가 하고 싶은 rebuild는 빨간색임 그러면 파란색이 stateful일 필요 없음
그럼 헤더에 행위가 있어야 하는데? 그럼 버튼이 빨간도화지에 같이 있어야 setState를 공유할 수 있음
⇒ 개념이 안잡혀있으면 그림을 바꿔버림(버튼 위치 변경 - 이러지 마세용)
⇒ 상태와 행위를 하나의 context에 모아두는 짓을 하게 됨..
Share article