[Flutter 공부하기] StatelessWidget vs StatefulWidget

반응형

이번에는 Flutter 앱 개발에서 가장 중요한 개념 중 하나인 '상태 관리'에 대해 살펴보겠습니다.
특히 Flutter의 기본 상태 관리 방식인 StatelessWidget과 StatefulWidget의 차이점과 각각의 사용법에 대해 자세히 알아보겠습니다.

상태(State)란?

Flutter에서 '상태'는 앱이 실행되는 동안 변할 수 있는 데이터를 의미합니다. 예를 들어 사용자가 체크박스를 클릭하면 체크 여부가 변경되고, 버튼을 누르면 화면의 데이터가 업데이트되며, 텍스트 필드에 글자를 입력하면 그 내용이 저장됩니다. 이런 모든 변화하는 데이터가 앱의 '상태'입니다.

자바나 자바스크립트에 비유하자면, 상태는 변수나 객체의 현재 값이라고 생각할 수 있습니다. 자바스크립트의 React에 익숙하신 분들은 state 개념이 매우 유사하다고 느끼실 겁니다.

Flutter에서 상태는 크게 두 가지 방식으로 관리됩니다.

  1. StatelessWidget - 상태가 없는 정적인 위젯
  2. StatefulWidget - 상태를 가지고 있어 동적으로 변할 수 있는 위젯

이 두 가지 위젯 타입의 차이점과 각각의 사용 방법에 대해 알아보겠습니다.

StatelessWidget 이해하기

StatelessWidget은 이름 그대로 '상태가 없는' 위젯입니다. 한 번 생성되면 그 내용이 변경되지 않으며, 같은 입력(속성)에 대해 항상 같은 출력(화면)을 보여줍니다.

자바의 불변(immutable) 객체나 자바스크립트의 순수 함수와 비슷한 개념이라고 생각하면 이해하기 쉽습니다. 항상 같은 입력에 대해 같은 출력을 보장합니다.

더 이해하기 쉽게 설명하자면, 한번 랜더링된 글자나 이미지, 아이콘 등은 절대 변하지 않습니다. 예를 들어, "안녕하세요!"라고 적힌 단어를 어떤 버튼을 눌러서 "반갑습니다!"라고 바꾸고 싶다면, StatelessWidget에서는 직접적으로 바꿀 수 없습니다.

StatelessWidget의 특징

  • 변경 불가능한 UI: 내부 상태를 가지지 않아 한 번 그려지면 변경되지 않음
  • 속성을 통한 데이터 전달: 생성자를 통해 외부에서 데이터를 받아 표시
  • 부모 의존성: 부모 위젯이 제공하는 데이터에만 의존함
  • 성능 이점: 상태 관리 로직이 없어 더 가벼움
  • 예측 가능성: 항상 같은 입력에 같은 출력을 보장

StatelessWidget 예제

import 'package:flutter/material.dart';

class GreetingCard extends StatelessWidget {
  final String name;
  final String message;

  const GreetingCard({
    Key? key, 
    required this.name, 
    required this.message
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: Colors.blue.shade100,
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            '안녕하세요, $name님!',
            style: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8.0),
          Text(message, style: TextStyle(fontSize: 16)),
        ],
      ),
    );
  }
}

위 예제는 자바스크립트의 함수형 컴포넌트(props만 받아 렌더링하는)나 자바의 불변 객체와 유사한 패턴입니다. 외부에서 데이터를 받아 UI를 구성하지만, 스스로 변경할 수는 없습니다.

StatelessWidget이 적합한 상황

  • 정적 화면 요소: 로고, 제목, 설명 텍스트 등 변하지 않는 UI 요소
  • 데이터 표시 전용: 부모로부터 받은 데이터를 단순히 보여주기만 하는 경우
  • 재사용 가능한 UI 컴포넌트: 버튼, 카드, 아이콘 등 상태가 필요 없는 재사용 컴포넌트
  • 성능 최적화 필요: 자주 다시 그려져야 하는 위젯의 경우 StatelessWidget 사용이 유리

자바스크립트 React 개발자라면 이것은 props만 받는 함수형 컴포넌트(useState 훅이 없는)와 유사하다고 생각하면 됩니다.

StatefulWidget 이해하기

StatefulWidget은 시간이 지남에 따라 변할 수 있는 상태를 가진 위젯입니다. 사용자 입력이나 비동기 작업 결과에 따라 UI가 변경되어야 할 때 사용합니다.

자바스크립트 개발자에게는 React의 state를 가진 컴포넌트나, 자바 개발자에게는 가변(mutable) 객체와 비슷한 개념입니다.

StatefulWidget의 특징

  • 내부 상태 관리: 위젯 내부에서 변경 가능한 상태를 가짐
  • 두 클래스 구조: 위젯 클래스와 상태 클래스로 분리된 구조
  • 자동 UI 업데이트: 상태 변경 시 자동으로 화면 갱신
  • 생명주기 관리: 위젯의 생성, 업데이트, 소멸 과정을 관리하는 메서드 제공
  • 상호작용 지원: 사용자 입력이나 이벤트에 반응하기 적합

StatefulWidget 예제

import 'package:flutter/material.dart';

// 1. StatefulWidget 정의
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

// 2. State 클래스 정의
class _CounterState extends State<Counter> {
  // 상태 변수 선언
  int _count = 0;

  // 상태를 변경하는 메서드
  void _increment() {
    // setState를 호출하여 상태 변경을 Flutter에 알림
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('현재 카운트: $_count', style: TextStyle(fontSize: 18)),
          ElevatedButton(
            onPressed: _increment,
            child: Text('증가'),
          ),
        ],
      ),
    );
  }
}

이 구조는 React 클래스 컴포넌트나 useState 훅을 사용하는 함수형 컴포넌트와 개념적으로 유사합니다. setState 메서드는 React의 setState와 비슷한 역할을 하며, 상태가 변경되면 UI가 다시 그려집니다.

StatefulWidget의 생명주기

StatefulWidget은 복잡한 생명주기를 가지고 있습니다. 자바스크립트 개발자에게는 React의 컴포넌트 생명주기 메서드와 비슷하다고 볼 수 있습니다.

주요 생명주기 메서드:

  1. initState() - 상태 초기화 (React의 componentDidMount와 유사)
  2. build() - UI 렌더링 (React의 render 메서드와 유사)
  3. setState() - 상태 업데이트 및 UI 갱신 트리거 (React의 setState와 유사)
  4. dispose() - 리소스 정리 (React의 componentWillUnmount와 유사)

Flutter를 처음 접하시는 분들은 모든 생명주기를 한번에 이해하려 하지 마시고, 가장 많이 사용하는 initState, setState, dispose를 중심으로 이해하면 좋습니다.

initState와 dispose 예시

class TimerWidget extends StatefulWidget {
  @override
  _TimerWidgetState createState() => _TimerWidgetState();
}

class _TimerWidgetState extends State<TimerWidget> {
  int _seconds = 0;
  late Timer _timer;

  @override
  void initState() {
    super.initState();
    // 컴포넌트가 생성될 때 타이머 시작 (React의 componentDidMount와 유사)
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        _seconds++;
      });
    });
  }

  @override
  void dispose() {
    // 컴포넌트가 제거될 때 타이머 정리 (React의 componentWillUnmount와 유사)
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('타이머: $_seconds초', style: TextStyle(fontSize: 24));
  }
}

자바스크립트에서 setInterval을 componentDidMount에서 설정하고 componentWillUnmount에서 정리하는 패턴과 매우 유사합니다.

StatefulWidget이 적합한 상황

  • 사용자 입력 처리: 폼, 텍스트 필드, 체크박스 등
  • 동적 데이터 표시: API 응답이나 비동기 데이터 표시
  • 애니메이션 처리: 상태 변화에 따른 애니메이션
  • 타이머나 스트림 사용: 주기적으로 업데이트되는 데이터

StatelessWidget과 StatefulWidget 비교

이해를 돕기 위해 두 위젯의 차이점을 자바/자바스크립트와 비교하며 설명해드리겠습니다:

특성 StatelessWidget StatefulWidget 자바/자바스크립트 비교
상태 없음 있음 불변객체 vs 가변객체
구조 단일 클래스 위젯+상태 클래스 순수함수 vs 클래스
리렌더링 부모 변경 시만 setState 호출 시 props 변경 vs state 변경
적합한 용도 정적 UI 동적 UI 데이터 표시 vs 사용자 상호작용
성능 가벼움 상대적으로 무거움 -
자바스크립트 비유 props만 있는 함수형 컴포넌트 useState/setState 사용 컴포넌트 -

실전에서의 효율적인 상태 관리

실제 앱 개발에서는 StatelessWidget과 StatefulWidget을 적절히 조합하여 사용하는 것이 중요합니다. 아래 팁들은 자바, 자바스크립트 개발 경험이 있는 분들도 쉽게 적용할 수 있습니다.

1. 상태는 필요한 곳에만 두기

자바스크립트 React의 "상태 끌어올리기(lifting state up)" 개념과 유사하게, 상태는 필요한 가장 낮은 레벨의 위젯에 두는 것이 좋습니다.

// 좋은 예: 상태는 필요한 위젯에만 배치
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 정적 UI는 StatelessWidget으로 구현
    return Column(
      children: [
        Header(),  // 정적 헤더
        CounterWidget(),  // 카운터 기능이 필요한 부분만 StatefulWidget
        Footer(),  // 정적 푸터
      ],
    );
  }
}

이렇게 하면 상태 변경 시 필요한 부분만 다시 그려지므로 성능이 향상됩니다.

2. 계산 가능한 값은 상태로 저장하지 않기

React의 useMemo나 computed 속성과 비슷한 개념으로, 다른 상태에서 계산할 수 있는 값은 별도의 상태로 저장하지 않는 것이 좋습니다.

// 좋은 예: 계산 가능한 값은 getter로 구현
class _CounterState extends State<Counter> {
  int _count = 0;

  // 계산이 필요한 값은 getter로 선언
  int get doubleCount => _count * 2;
  bool get isEven => _count % 2 == 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        Text('Double: $doubleCount'),
        Text(isEven ? '짝수입니다' : '홀수입니다'),
      ],
    );
  }
}

3. const 생성자 활용하기

const 생성자는 자바스크립트 React의 React.memo()나 PureComponent와 비슷한 역할을 합니다. 변경되지 않는 위젯은 const로 선언하여 Flutter가 위젯을 재사용할 수 있게 합니다.

// 변경되지 않는 위젯은 const로 선언
const Text('안녕하세요'),
const SizedBox(height: 8),
const Padding(
  padding: EdgeInsets.all(16.0),
  child: Icon(Icons.star),
),

사소해 보일 수 있지만, 이런 최적화가 모여 앱의 전반적인 성능을 크게 향상시킬 수 있습니다.

자바/자바스크립트 개발자를 위한 Flutter 상태 관리 이해하기

이미 다른 언어로 개발 경험이 있으신 분들을 위해 Flutter의 상태 관리를 비교해보겠습니다.

자바스크립트 React 개발자의 경우.

  • StatelessWidget = 순수 함수형 컴포넌트 (props만 사용)
  • StatefulWidget = useState 훅이나 class 컴포넌트 (state 관리)
  • setState = React의 setState 또는 useState의 setter 함수
  • initState = componentDidMount 또는 useEffect(() => {}, [])
  • dispose = componentWillUnmount 또는 useEffect의 cleanup 함수

자바/안드로이드 개발자의 경우.

  • StatelessWidget = 읽기 전용 View 컴포넌트
  • StatefulWidget = 상태를 가진 Activity/Fragment 또는 LiveData 활용 컴포넌트
  • setState = LiveData의 setValue() 또는 notifyDataSetChanged()와 유사
  • initState = onCreate(), onStart()와 유사
  • dispose = onDestroy()와 유사

상태 관리 솔루션 소개

Flutter 생태계에는 다양한 상태 관리 솔루션이 있습니다. 자바스크립트 개발자들에게 친숙한 비유로 설명하자면:

  1. Provider - React의 Context API와 유사
  2. Bloc/Rx - Redux + RxJS와 비슷한 개념
  3. GetX - 통합 상태 관리 솔루션
  4. Riverpod - Provider의 개선 버전
  5. MobX - 자바스크립트의 MobX와 동일한 개념

각 솔루션은 특정 상황에 더 적합할 수 있으며, 앱의 복잡도와 개발 팀의 선호도에 따라 선택하면 됩니다. 저는 Riverpod을 주로 이용하고 있습니다.

 

이번 글에서는 Flutter의 기본 상태 관리 방식인 StatelessWidget과 StatefulWidget에 대해 살펴보았습니다. 감사합니다.

Designed by JB FACTORY