[Flutter] 상태 관리 1편(State Management) - State Ful

Tyger·2023년 1월 7일
4

State Management

목록 보기
1/14

상태 관리(State Management) 1편 - State Ful

상태 관리(State Management) 2편 - Value Listenerable
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 4편 - Get X [Reactive]
상태 관리(State Management) 5편 - Provider
상태 관리(State Management) 6편 - Bloc
상태 관리(State Management) 7편 - Cubit
상태 관리(State Management) 8편 - Riverpod
상태 관리(State Management) 9편 - Mobx

Top 7 Flutter State Management Libraries In 2022
Most Popular Packages for State Management in Flutter (2023)

Dart Packages

개발을 하면서 가장 기본적이면서 무조건 사용하게 되는 기능이 바로 상태 관리(State Management)이다.

상태 관리라는 것은 앱이 실행되는 동안 앱 내에 다양하게 존재하는 상태들을 관리하면서 개발을 하는 것을 의미한다.
처음에는 상태 관리가 이해가 되지 않겠지만 데이터를 관리한다고 생각하면 된다.

상태 관리를 하지 않고 개발되는 앱은 거의 없을 것이며, 상태 관리를 하는 방식에 차이가 있을뿐 꼭 필요하다.

Flutter에는 다양한 상태 관리 라이브러리들이 있는데, 구글 Dev팀이 직접 개발한 라이브러리도 있고, 개인이 배포한 유명한 라이브러리들이 있다.

현업에서 GetX를 사용하고 있고 이전 개발사에서는 Bloc 패턴을 메인으로 사용하였고 개인적인 프로젝트에서는 주로 provider를 사용하고 있는데 개인적인 관점에서는 너무 작은 프로젝트가 아닌 이상 Bloc 패턴 또는 Bloc+provider를 사용하는 것을 가장 선호하고 있다. GetX는 정말 편리하고 상태 관리 외에도 많은 기능을 가지고 있는 라이브러리라 강력하고 좋은 것은 알겠지만, 글쎄.. 앞으로는 상태 관리 라이브러리로는 사용하지 않을 듯 싶다.

상태 관리에 대해서 공식적인 레퍼런스나 예제 파일을 보면 간단한 기능만을 가지는 상태 관리로 설명을 하다보니 초보자는 이해가 잘 되지 않고 하나의 상태가 아닌 여러 상태를 가지고 있어야 할 때는 어떻게 사용을 해야 하는가 모를 때가 많다.

앞으로 상태관리 라이브러리에 대해서 여러 편으로 나눠서 작성을 할 예정이다

Count App

카운터 앱은 Flutter 프로젝트 최초 생성시 기본으로 있는 카운트 앱을 약간 변형하여 리셋 기능을 추가하고 단순히 카운트 상태를 증가/감소만 하는 것이 아닌 얼마 만큼을 증가/감소 시킬지에 대한 상태를 추가하여 해당 값 만큼 증가/감소하는 기능을 가지게끔 만든 예제이다.

모든 상태관리 예제는 해당 기능을 가진 카운트 앱으로 만들어 볼 것이다.

StatefulWidget

Flutter를 처음 접할 때 가장 익숙한 위젯이 바로 StatefulWidget이다. Flutter에는 크게 StatelessWidget과 statefulWidget이라는 두 개의 위젯으로 나눌 수가 있는데, 말 그대로 상태가 없는 위젯과 상태가 있는 위젯이라고 생각하면 된다.

statefulWidget은 빌드 안에서 setState 라는 기능안에 상태 값을 변경시켜 빌드의 state에 변경 알림을 보냄으로써 상태 값이 변한 것을 빌드하여 보여주는 위젯이다.

개인적으로 statefulWidget은 개발을 하면서 실제 사용한 적은 거의 없는 것 같다. 간단한 뷰에서 상태를 처리할 때도 주로 statelessWidget에 ValueNotifier로 처리하는게 더 편해서 그렇게 하고 있는 편이다.

StatefulWidget이 StatelessWidget보다 얼만큼의 퍼포먼스의 차이를 느끼는 지는 못하지만 flutter 처음 접했을 때부터 statelessWidget에 상태 관리로 개발을 하다보니 사용하지 않은 것이지 필요한 순간이 있다면 사용하는 것도 나쁘지는 않은 것 같다.

UI

앞으로 모든 상태관리에 동일한 UI파일을 사용할 거여서 상태관리 편에서 UI 내용은 다른 글과 동일할 것이다.

UI는 가운데 카운트를 보여줄 숫자가 있고 바로 하단 Row위젯안에 더하기, 마이너스 아이콘을 배치해뒀다. 그 아래로 reset 기능을 호출할 버튼을 만들었다.

카운트 기능을 사용하는게 단순히 숫자만 올리고 내리는 것이 아니라 얼만큼을 증가시키고 감소시킬지를 선택할 수 있는 넘버 박스들을 왼쪽 상단에 수직으로 배치하여 구성하였다.

여기서는 간단한 상태 관리만 보여주는 정도의 UI여서 다른 글에서 각각의 상태 관리에 대해서 더 깊숙하고 복잡한 UI 구조를 만들어서 사용해 볼 예정이다.

아래 공유한 Git Repository를 방문하면 소스 코드를 오픈해 뒀습니다 !

 return Scaffold(
      appBar: appBar(title: 'Count App With State Ful'),
      body: countScreenPublicUI(
        context: context,
        count: _count,
        selectCount: _selectCount,
        onIncrement: _increment,
        onDecrement: _decrement,
        onReset: _reset,
        onCount: _select,
      ),
    );
Stack countScreenPublicUI({
  required BuildContext context,
  required int count,
  required int selectCount,
  required Function() onIncrement,
  required Function() onDecrement,
  required Function() onReset,
  required Function(int) onCount,
}) {
  return Stack(
    children: [
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          SizedBox(
              width: MediaQuery.of(context).size.width,
              child: Center(
                child: Text(
                  "$count",
                  style: const TextStyle(
                      fontSize: 60, fontWeight: FontWeight.bold),
                ),
              )),
          const SizedBox(height: 24),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              GestureDetector(
                onTap: onIncrement,
                child: const Icon(
                  Icons.add_circle_outline,
                  size: 40,
                ),
              ),
              const SizedBox(width: 24),
              GestureDetector(
                onTap: onDecrement,
                child: const Icon(
                  Icons.remove_circle_outline,
                  size: 40,
                ),
              )
            ],
          ),
          const SizedBox(height: 24),
          GestureDetector(
            onTap: onReset,
            child: Container(
              width: MediaQuery.of(context).size.width / 3,
              height: 48,
              decoration: BoxDecoration(
                  color: const Color.fromRGBO(71, 71, 71, 1),
                  borderRadius: BorderRadius.circular(12)),
              child: const Center(
                child: Text(
                  'Reset',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
          const SizedBox(height: 40),
        ],
      ),
      Positioned(
        top: 20,
        child: SizedBox(
          height: MediaQuery.of(context).size.height,
          child: Padding(
            padding: const EdgeInsets.only(left: 20),
            child: Column(
              children: [
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 1),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 10),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 20),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 50),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 100),
              ],
            ),
          ),
        ),
      ),
    ],
  );
}
GestureDetector countAppSelectedCountBox({
  required Function(int) onTap,
  required int number,
  required int selectNumber,
}) {
  return GestureDetector(
    onTap: () => onTap(number),
    child: Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Container(
        width: 48,
        height: 48,
        decoration: BoxDecoration(
            color: selectNumber == number
                ? const Color.fromRGBO(91, 91, 91, 1)
                : const Color.fromRGBO(61, 61, 61, 1),
            borderRadius: BorderRadius.circular(12)),
        child: Center(
            child: Text(
          '$number',
          style: TextStyle(
              fontWeight: FontWeight.bold,
              color: selectNumber == number
                  ? Colors.white
                  : const Color.fromRGBO(155, 155, 155, 1)),
        )),
      ),
    ),
  );
}

SetState

먼저 StatefulWidget 안에 state 아래에 사용할 변수를 선언해 준다. count 변수는 실제 카운트가 되어서 보여질 변수이고, selectCount 변수는 얼만큰 증감을 시킬지를 선택하는데 사용하는 변수이다.

selectCount 변수의 기본 값은 1로 선언하여 처음 진입 시 1번 박스에 선택될 수 있도록 하였다.

 int _count = 0;
 int _selectCount = 1;

setState는 state의 변화를 호출하는 기능인데, 해당 state는 우리가 위에서 선언한 변수 부터 state안에 포함된 모든 곳에 빌드를 다시 시켜 상태의 변화를 UI로 보여주게 된다.

여러 상태관리 툴과 다른 점이 setState는 state아래에 있는 모든 UI를 다시 빌드하는 것에 있어서 리소스 낭비가 있다고 볼 수 있다.

setState는 함수 형태((){})안에 setState((){}); 안에다 변경할 변수의 값을 넣어주면 된다.
매우 간편하게 사용할 수 있으며, StatefulWidget의 setState를 사용해봐야 상태 관리를 왜 사용해야 하는지 이해가 더 빠르게 될 것이다.

reset()에서는 count 변수를 0으로 만들어 주고 있고, select()는 int형 파라미터를 받아와 해당 int를 selectCount 변수로 변경해주고 있으며 increment()/decrement()는 현재 카운트 값에 선택한 selectCount 값을 증감 시켜서 작동하고 있다.

  void _reset() {
    setState(() {
      HapticFeedback.mediumImpact();
      _count = 0;
    });
  }

  void _select(int number) {
    setState(() {
      HapticFeedback.mediumImpact();
      _selectCount = number;
    });
  }

  void _increment() {
    setState(() {
      HapticFeedback.mediumImpact();
      _count = _count + _selectCount;
    });
  }

  void _decrement() {
    setState(() {
      HapticFeedback.mediumImpact();
      _count = _count - _selectCount;
    });
  }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/blob/main/lib/count_app/count_screen_with_stateful.dart

마무리

StatefulWidget은 flutter를 접할 때 가장 처음 접하게 되는 위젯이라 기본적으로 이해를 하고 있었을 것이다.

다음에는 statefulWidget을 사용하지 않고 statelessWidget을 상태관리 라이브러리 없이 변경할 수 있는 flutter 기본 기능인 ValueNotifier에 대해서 작성하도록 하겠다.

profile
Flutter Developer

3개의 댓글

comment-user-thumbnail
2023년 6월 22일

좋은 글 감사합니다.

답글 달기
comment-user-thumbnail
2023년 11월 15일

안녕하세요! 본문에 GetX는 더 이상 사용하지 않으실 거라고 하셨는데 이유가 뭔지 궁금합니다!

1개의 답글