[Flutter] Widget이란? Stateful vs Stateless
플러터에선 위젯을 써서 UI를 구현한다. 그리고 이 위젯은 Stateful하거나 Stateless하거나 둘 중 하나의 특징을 갖는다.
Stateful, Stateless를 확인하려면 위젯이 뭔지 알아야 이해가 좀 더 쉬울 것 같아 위젯부터 확인한다.
플러터 위젯은 리액트에서 영감을 얻은 프레임워크를 써서 제작됐다. 핵심 아이디어는 위젯으로 UI를 구축한다는 것이다. 위젯은 현재 구성, 상태에 따라 뷰가 어떤 모습이어야 하는지 설명한다. 위젯 상태가 바뀌면 위젯은 description을 재작성하고, 프레임워크는 기본 렌더 트리에서 한 상태에서 다음 상태로 전환하는 데 필요한 최소한의 변경을 결정하기 위해 이전 description과 비교해서 변경한다
import 'package:flutter/material.dart';
void main() {
runApp(
const Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
runApp()은 지정된 위젯을 받아 위젯 트리의 루트로 만든다. 위 예시에서 위젯 트리는 중앙 위젯, 그 자식인 Text 위젯으로 구성된다. 프레임워크는 루트 위젯이 화면을 덮게 강제하므로 "Hello, World!"라는 텍스트가 화면 중앙에 표시된다
앱을 작성할 때 위젯이 상태를 관리하는지 여부에 따라 일반적으로 StatefulWidget 또는 StatelessWidget의 서브클래스인 새 위젯을 작성하게 된다. 위젯의 주요 역할은 다른 하위 수준 위젯의 관점에서 위젯을 설명하는 build()를 구현하는 것이다. 프레임워크는 위젯의 지오메트리를 계산하고 설명하는 기본 렌더 객체를 나타내는 위젯에서 프로세스가 끝날 때까지 이런 위젯을 차례로 빌드한다...(중략)
https://api.flutter.dev/flutter/widgets/Widget-class.html
위젯은 플러터 프레임워크의 중심 클래스 계층 구조다. 위젯은 UI 일부에 대한 불변의 설명이다. 위젯은 기본 렌더 트리를 관리하는 요소로 부풀려질 수 있다
위젯 자체에는 변경 가능한 상태가 없다(모든 필드는 final 상태여야 함). 변경 가능한 상태를 위젯에 연결하려면 element로 인플레이션되어 트리에 통합될 때마다 StatefulWidget.createState를 통해 State 객체를 생성하는 StatefulWidget 사용을 고려하라...(중략)
https://docs.flutter.dev/ui/interactivity#stateful-and-stateless-widgets
위젯은 Stateful 또는 Stateless 중 하나다. 예를 들어 유저가 위젯과 상호작용할 때 위젯이 변경될 수 있다면 Stateful이다. Stateless 위젯은 절대 변경되지 않는다. Icon, IconButton, Text가 Stateless 위젯의 예시다
Stateful 위젯은 동적이다. 유저 상호작용에 의해 트리거된 이벤트에 반응하거나 데이터를 수신할 때 모양을 바꿀 수 있다. 체크박스, 라디오, 슬라이더, 잉크웰, 폼, 텍스트 필드가 Stateful 위젯의 예시다
위젯 상태는 State 객체에 저장되서 위젯의 상태, 모양을 분리한다. State는 슬라이더의 현재 값이나 체크박스의 체크 여부와 같이 변경할 수 있는 값으로 구성된다. 위젯 상태가 바뀌면 State 객체는 setState()를 호출해서 프레임워크에 위젯을 다시 그리게 지시한다
공식문서에서 말하는 핵심은 위젯을 통해 UI를 구현하며, 위젯 자체에는 변경 가능한 상태가 없지만 StatefulWidget이나 StatelessWidget을 확장하고 build()에 어떤 형태로 위젯을 표시할지 개발자가 정해주는 것이다. 이 결과로 앱, 웹 화면에 정의된 위젯이 표시되고 사용자가 보거나 상호작용할 수 있게 된다.
안드로이드의 버튼, 체크박스, EditText 등의 뷰들을 플러터에선 위젯이라고 표현한다. 플러터가 제공하는 위젯들은 아래 링크에서 카탈로그 형태로 확인할 수 있으니 참고한다. 당연히 외울 필요는 없고 필요할 때 사용하다 보면 자연스럽게 몸에 익을 것이다.
https://docs.flutter.dev/ui/widgets
다른 곳에선 StatefulWidget, StatelessWidget을 어떻게 설명하는지 확인한다.
https://medium.com/@gunseliunsal/stateless-vs-stateful-widgets-in-flutter-852741b6046e
StatelessWidget은 일단 생성되면 컨텐츠, 상태를 수정할 수 없는 위젯으로 작동한다. 간판, 포스터와 비슷하다. 간판은 변하지 않는 고정된 메시지나 정보를 표시한다. 사람들은 지나가면서 그 내용을 보고 정보를 얻을 수 있지만 간판 자체는 변하지 않고 처음과 동일한 메시지를 전달한다. 마찬가지로 StatelessWidget은 처음에 주어진 데이터로 UI를 생성하고 이후엔 변경되지 않는다
StatefulWidget은 체스 게임과 유사하다. 많은 말이 움직이지만 움직이는 말이 하나만 있어도 상태 변경으로 간주한다. 여러 변경이 필요하지 않다
StatelessWidget을 StatefulWidget으로 바꾸려면 단 한 번의 변경만 필요하다. StatelessWidget은 변경사항이 없다
https://www.linkedin.com/pulse/stateful-stateless-widgets-flutter-anastasia-wartell/
StatelessWidget은 바뀌지 않는다. 일단 생성되면 수정할 수 없다. 플러터는 위젯을 한 번만 신경쓰면 되며 복잡한 상태를 유지하거나 코드를 수정할 필요가 없다. StatelessWidget을 수정하는 유일한 방법은 위젯을 삭제하고 새 위젯을 생성하는 것이다
모든 StatelessWidget의 핵심은 build()다. 플러터는 주어진 위젯 집합을 다시 그려야 할 때마다 이 메서드를 호출한다. 기기를 회전하거나 앱을 닫을 때 발생할 수 있다
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
StatefulWidget은 정보(상태)를 보관하고 상태가 바뀔 때마다 스스로를 다시 생성하는 법을 알고 있다. 모든 StatefulWidget에는 생명주기를 유지하는 State 객체가 필요하다. 이것은 완전한 별도의 클래스다
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
StatefulWidget에선 build()를 위젯이 아닌 State 클래스에 넣는다. StatefulWidget은 위젯, State의 2가지 클래스로 구성된다
StatefulWidget의 위젯 부분은 실제로 많은 일을 하지 않는다. 바뀌는 데이터는 State 클래스에 있어야 한다
참고) Stateful이든 Stateless든 모든 위젯은 불변이다. StatefulWidget에선 상태가 변경될 수 있다
위 내용을 보면 알겠지만 Stateful과 Stateless의 결정적 차이는 상태를 갖는지 여부다.
상태를 갖지 않으면서 위젯의 생명주기 동안 처음 만들어진 상태 그대로 유지되어야 한다면 Stateless로 구현하고, 상호작용에 따라 유저에게 보여지는 값이 달라지거나 서버에서 받아온 데이터를 표시해야 한다면 Stateful로 구현하는 것이다.
그러나 StatefulWidget으로 구현하더라도 반드시 변하는 값이 위젯에 표시되진 않는다.
위에서 확인했듯 State 여부에 상관없이 모든 위젯은 불변이며 StatefulWidget은 위젯의 상태를 변경할 수 있는 불변 위젯이다. 그래서 값이 바뀌었음을 플러터 프레임워크에 알리는 장치가 필요한데 그 역할을 하는 메서드가 setState()다.
https://api.flutter.dev/flutter/widgets/State/setState.html
이 객체의 내부 상태가 바뀌었음을 프레임워크에 알린다. State 객체의 내부 상태를 바꿀 때마다 setState에 전달하는 함수에서 변경을 수행한다
setState(() { _myState = newValue; });
제공된 콜백은 즉시 동기식으로 호출된다. 상태가 실제로 설정되는 시점이 불분명해지므로 future를 리턴해선 안 된다(콜백은 비동기일 수 없음). setState를 호출하면 이 서브트리의 UI에 영향을 줄 수 있는 방식으로 이 객체의 내부 상태가 바뀌었음을 프레임워크에 알리고, 프레임워크는 이 State 객체에 대한 빌드를 예약하게 된다
setState를 쓰지 않고 상태를 직접 바꾸면 프레임워크가 빌드를 예약하지 않고 이 하위 트리의 UI가 새 상태를 반영하도록 업데이트되지 않을 수 있다
일반적으로 setState는 변경과 연관될 수 있는 계산이 아닌 상태에 대한 실제 변경을 래핑하는 데만 쓰는 게 좋다...(중략)...이 함수를 호출하면 위젯이 재빌드되고, 이 위젯에 루팅된 전체 서브트리의 재빌드가 수행될 수 있으며...(중략)...간접 비용이 많이 든다. 따라서 이 메서드는 build()가 감지된 상태 변경의 결과가 의미있게 바뀔 때만 호출해야 한다
그래서 StatefulWidget의 내부 상태가 바뀌었음을 플러터 프레임워크에 알려서, 변경된 값을 표시하도록 위젯을 다시 그리게 하려면 setState()를 호출해야 한다.
그러나 위젯의 재빌드가 하나의 위젯이 아닌 여러 위젯에서 발생한다면 성능 저하는 필연적이다. 그러니 불필요한 위젯까지 같이 재빌드되지 않도록 신중하게 사용하는 게 중요하다.
추가로 build() 안에서 setState()를 호출하면 안 된다. 왜냐면 setState()를 호출할 때마다 새로운 상태가 있을 경우 build()가 재귀적으로 호출되기 때문이다. 버튼 클릭, 유저 입력 트리거 같은 1회성 이벤트에서 사용하는 게 좋다.