플러터로 앱을 개발할 때, 자동완성 기능을 이용해 stful이나 stless 단축어를 우선 입력하고 이후 리턴문 뒤에 구성하고자 하는 인터페이스 컴포넌트들을 작성하곤 한다. 이는 Stateful, Stateless의 약자로, State(상태)의 취급 의도에 따른 구분을 둔 것이다. 그렇다면 이 State는 무엇이고, 왜 이들을 구분해야 하는 것일까?
💬 Class와 Widget의 개념을 잘 모른다면, Class와 Widget 알아보기 글을 먼저 읽어주세요.
🔎 State의 의미
State는 말 그대로 '상태'이다. 사용자 인터페이스(UI)를 구성하는 요소들의 변화나 데이터의 변경등을 일컫는 상태를 말한다. 또한 State는 두 가지로 나뉜다. App state와 Widget(UI) state는 각각 앱 전체적인 상태와 위젯 내부의 상태를 뜻하며, 여기서 사용되는 stateful, stateless가 Widget state를 구분하는 것이다.
💬 App state
앱 전체에서 공유되는 상태를 의미한다. 사용자 로그인 정보, 앱 설정, 테마 설정 등과 같이 앱 전반에 걸쳐서 공유되는 상태를 관리하는 것이 주요 목적이다. '상태 관리 라이브러리'와 연관 있는 개념이며, 장바구니와 같이 전역적인 상태를 관리할 때 필요하다.
💬 Widget state
플러터에서 Stateful Widget이나 StatelessWidget에서 사용되는 개념으로, 해당 위젯의 내부에서 유지되는 상태를 의미한다. Widget state는 StatefulWidget에서 관리되는 경우 상태가 변경될 때마다 해당 위젯이 다시 빌드되고 UI가 업데이트된다. 반면에 StatelessWidget은 상태를 가지지 않으므로 Widget State가 없다.
Widget state의 구분
윗 단락을 통해 State는 앱과 위젯, 두 가지로 구분되며, Stateful - Stateless로 구분되는 인터페이스의 상태는 Widget State의 개념과 관련이 있다는 사실을 알게 되었다.
그렇다면 Stateful과 Stateless는 정확히 어떨 때 각각 사용되어야 하는 것일까?
✅ Stateful
StatefulWidget은 State가 존재하는 위젯을 생성하는 데 사용되는 클래스를 말한다. StatefulWidget은 변경 가능한 상태를 유지하고 있으며, 위젯이 다시 렌더링 될 때 해당 상태가 변경될 수 있다. 버튼을 누르거나 무언가를 로딩할 때와 같이 사용자와 상호작용해야 하는 경우, StatefulWidget 클래스를 상속하여야 한다.
✅ Stateless
StatelessWidget은 State가 '없는' 클래스로 해석해야 할까? 정확히 하자면, 상태가 '변하지 않는' 클래스라고 보아야 할 것이다. 클래스를 구성하는 위젯과 인터페이스는 한 번 렌더링 되면 이후 변경되지 않고 변경할 수 없다. Stateless Widget은 주로 정적인 UI 요소를 만드는 데 사용되며, 위젯의 렌더링은 부모 위젯이 상태를 변경하거나 외부 요소가 영향을 주는 것이 아닌 자체 내부 속성에만 의존한다.
StatelessWidget을 사용하는 이유?
그렇다면, 단순한 생각으로 '모든 클래스를 StatefulWidget으로 사용하는 것이 더 합리적이지 않을까?' 하는 의문이 들 수도 있다. 구성요소들이 변하든 변하지 않든, '변할 수 있는' Stateful에 모두 때려 넣으면 편할 테니 말이다.
하지만 Stateless는 분명히 사용해야 할 이유들이 있다. Stateless Widget은 상태를 가지지 않고, 불변하며, 한 번 렌더링된 이후에는 내부 상태가 변경되지 않는다는 것은, 다시 말해 Stateful Widget보다 메모리 사용과 성능 면에서 더 우수하며, 동일한 UI를 구현하는 데에도 더 적은 코드를 필요로 한다는 말과 같다.
따라서 Stateful과 Stateless를 필요한 곳에 조화롭게 사용하여야 효율적인 코드를 작성할 수 있다.
🔎 Stateless
아래는 처음 dart 파일을 작성할 때, 단축어 'stless'를 입력하면 완성되는 가장 기본적인 StatelessWidget 상속 클래스이다.
class ______ extends StatelessWidget {
const ______({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
물론 _____ 안에는 ('MyApp'과 같은) 내가 작성한 클래스명이 들어가게 될 것이다.
이 코드를 가장 바깥을 감싸는 괄호서부터 읽어보자.
class ______ extends StatelessWidget {
...
}
StatelessWidget을 상속받는다. StatelessWidget 클래스는 flutter/framework.dart 패키지에 추상 클래스로 정의되어 있다. 이 클래스를 상속하여 새로운 위젯을 만들 때, build() 메서드를 반드시 오버라이드해야 한다. 왜냐하면, 조금 아래에 나오겠지만, build()의 BuildContext가 갖는 의미가 아주 크기 때문이다.
...
@override
Widget build(BuildContext context) {
return const Placeholder();
}
...
그래서 build()를 오버라이딩한다. build는 위젯의 렌더링 결과를 반환한다. 그렇다면 (반드시 필요한) 인터페이스를 구성하는 위젯과, 그 컴포넌트들을 모아놓은 하나의 클래스 사이에 build()는 왜 필요하고 어떤 역할을 하는 것일까?
build()와 context 이해하기
아직은 이해하기 난해할 수도 있으나, build가 갖는 BuildContext형의 파라미터는 위젯 트리의 노드에 해당하는 정보를 제공하며, 기능과 리소스에 접근할 수 있는 일종의 포인터 역할을 한다. 예를 들어 버튼을 눌러 화면을 이동하거나, 또는 인터페이스 내의 구성 요소를 변경해야 한다면, 층층이 쌓인 위젯들에서 '어디부터 어디까지'를 변경해야 하는지를 알아야 할 것이다.
💬 더 쉽게 비유하자면, BuildContext는 메이플스토리 '마을귀환 주문서'이다. 헤네시스 골렘 사냥터에서 주문서를 사용하면 당연히 헤네시스로 가고, 에레브 수련장에서 쓰면 에레브로 갈 것이다. 만약 헤네시스에서 '마을귀환 주문서'를 썼는데 1레벨 때 처음 밟은 '메이플아일랜드'로 돌아가면, 그건 황당한 일이다.
context, 우리말로 "문맥"은 바로 우리가 돌아갈 알맞은 '마을'을 찾아준다. 헤네시스로, 에델슈타인으로 잘 찾아가도록 군데군데 깃발을 꽂아주는 것이다.
다음 글에 언급될 내용으로, 이벤트 리스너(onPressed 등)가 버튼 클릭 등을 감지했을 때, 이전으로 돌아가거나 다른 화면으로 넘어가는 일을 해야 한다면, 이 context가 정말 중요한 역할을 한다는 것을 다시 알 수 있을 것이다. 이외에도 미디어 쿼리, 라이브러리 등 다양한 곳에 사용된다. 또한 builder:,를 사용하여, 필요할 때면 context를 위젯 트리의 중간중간 더 넣을 수 있다. (이후에 더 자세히 설명한다)
따라서 핵심은, 변하지 않는 StatelessWidget을 사용할 때, build()를 반드시 오버라이드해야 하며, 그 반환값이 인터페이스를 구성하는 위젯이 된다는 것이다.
🔎 Stateful
만약 StatelessWidget으로 작성한 클래스를 StatefulWidget으로 변경하고 싶다면 어떻게 해야할까? 대부분의 IDE는 이미 이 기능을 자동완성으로 지원하고 있다.
StatelessWidget을 누르고 좌측의 전구(💡)나 'Alt+Enter' 단축키를 눌러 사용자 제안을 켜면, 즉시 StatefulWidget으로 전환할 수 있다.
다만, 자동 기능을 자주 사용하더라도 Stateful과 Stateless 둘 사이에 어떤 점이 다른지는 짚고 넘어가는 것이 좋다.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
위는 단축어 'stful'을 입력하면 완성되는 가장 기본적인 StatefulWidget 상속 클래스이다. 코드를 보아 알 수 있듯, Stateless와 Stateful 모두 다 build() 위젯을 오버라이드하며, 반환값으로 필요한 부분의 위젯들을 렌더시킨다는 공통점이 있다.
가장 주요한 차이점으로는, stateful은 stateless와 다르게 클래스가 위아래 총 2개로 이루어진다.
첫번째 클래스는 StatefulWidget을 상속받고, 두번째 클래스는 State<T>를 상속받는다.
두 클래스로 나뉜 이유는, (첫번째 클래스가 상속받는) 'StatefulWidget'이 상속받는 Widget이 immutable(불변)하기 때문이다. StatefulWidget은 가변하는 컴포넌트를 담아야 하는데, StatefulWidget은 불변한다. 겉보기에는 상당히 모순적이다.
그렇기에 한 번 생성되면, StatefulWidget만으로는 StatelessWidget과 마찬가지로 상태가 변하지 않는다. 왜 Widget은 mutable하지 않은지, 왜 Widget을 상속하여 클래스를 2개로 나눠야만 하는지에 대해 많은 의문이 있을 것으로 생각된다.
해당 글에 그 이유에 대해 자세히 설명되어 있는데, 요약하자면 두 개의 클래스로 분리함으로써 StatefulWidget API가 불변성을 유지하고, 'State<T>'가 가변성을 유지할 수 있게끔 만들었다고 한다. 여기서 'State<T>'는 바로 아래에 이어 설명한다.
Stateful의 첫번째 클래스
///stateful을 구성하는 첫 번째 클래스
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
편의를 위해, Stateful을 두 클래스 각각 나누어 살펴보자.
첫번째 클래스가 상속받는 StatefulWidget은 반드시 'State<T>' 객체를 생성하는 createState() 메서드를 오버라이드해야 한다.
💬 => (fat arrow) 가 뭔가요?
dart에서의 함수 표현 방식 중 하나인데, 중괄호를 생략한 반환 표기법임을 이해할 수 있는 좋은 글이 있다.
원문 소스코드를 보아 알 수 있지만, State의 제네릭 타입 <T>는 'StatefulWidget'이어야 한다. 따라서, State는 StatefulWidget을 상속받은 첫번째 클래스 이름(MyApp)을 아래와 같이 제네릭 타입에 넣어준다.
/// 첫번째 클래스의 마지막 줄
State<MyApp> createState() => _MyAppState();
Stateful의 두번째 클래스
///stateful을 구성하는 두번째 클래스
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
이 클래스까지 이해한다면, 구성이 조금 보이기 시작할 것이다.
아까 첫번째 클래스는 createState()로 _MyAppState()를 반환했고, 두번째 클래스인 _MyAppState()는 제너릭(<T>)으로 <MyApp>을 던진다. 두 클래스는 이런 방식을 통해 하나로 연결되는 것이다.
또한 State 클래스는 build()를 오버라이드하여, 인터페이스를 반환해야 한다. 해당 반환문 내에는 Stateless와 다르게, Stateful한 컴포넌트들(체크박스, 라디오버튼 등)이 포함될 수 있게 된다.
💬 State 작명하기
일반적으로, State는 '_(언더스코어)' + '[[클래스명]]' + 'State' (예시:_MyAppState) 식으로 작명합니다.
마치며
플러터와 다트를 다루면 반드시 가장 처음 사용하게 되는 형식과 개념으로써 상세히 짚고 넘어갈 필요를 느껴 초반에 State 관련 내용을 다루게 되었다. 당장 이해하지 못해도, '다른 쉬워요' 글들을 천천히 읽고 나서 보아도 좋을 것 같다.
'👨💻 프로그래밍 언어 > 플러터' 카테고리의 다른 글
플러터(Flutter) 반드시 해주는 초기 설정 · 프로젝트 구조 파악하기 (0) | 2023.08.04 |
---|---|
플러터(Flutter) 프로젝트 생성 · 앱 실행해보기 (0) | 2023.08.01 |
플러터(Flutter) 설치 · 안드로이드 스튜디오에서 개발환경 구축하기 (0) | 2023.07.31 |