플러터의 Stateful UI

윤뿔소·2023년 5월 30일
0

Dart / Flutter

목록 보기
15/18

그 전까지 정적인 Stateless UI를 만들었다.

// StatelessWidget
class App extends StatelessWidget {
  const App({super.key});

  
  Widget build(BuildContext context) {

이제 상태 값을 가진 Stateful UI를 만들어 보자. 그 전에 설명을 좀 하고 넘어가겠다.

Stateful UI

플러터에서 Stateful UI는 상태(state)를 가지고 있는 사용자 인터페이스를 구현하는 방법이다.

동적인 UI를 만들고 사용자와의 상호작용에 따라 상태가 변하여 UI를 업데이트할 수 있게 해줌.
데이터의 변경에 따라 UI를 실시간으로 업데이트할 수 있음.

StatefulWidget을 확장하여 클래스를 만든다. StatefulWidget은 두 가지 클래스로 구성된다.

  1. StatefulWidget 클래스는 변경 가능한 상태를 포함
  2. State 클래스는 해당 상태를 관리하고 UI를 업데이트하는 역할을 담당

StatefulWidget은 사용자의 상호작용에 따라 상태를 변경하고, State 클래스는 변경된 상태에 따라 UI를 다시 렌더링

Stateful UI를 구현하는 과정은 다음과 같다:

  1. StatefulWidget 클래스를 상속하는 위젯 클래스를 작성
  2. createState() 메소드를 오버라이드하여 해당 위젯의 상태를 관리하는 State 클래스를 생성
  3. State 클래스에서 build() 메소드를 오버라이드하여 UI를 렌더링
  4. 상태가 변경되면 setState() 메소드를 호출하여 변경된 상태를 알림

이에 따라 플러터는 자동으로 build() 메소드를 호출하여 UI를 업데이트한다.

예시

플러터에서 구현하는 방법은 되게 간단하다. 스니펫을 사용하거나 코드 액션을 사용하면 된다.

스니펫

st만 입력해도 저렇게 나오고, 저걸 입력하면

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

이렇게 된다.

코드 액션

Stateless 클래스를 클릭 후 코드 액션 클릭하면

class App extends StatefulWidget {
  const App({super.key});

  
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  
  Widget build(BuildContext context) {
  ...

State를 가진 클래스를 포함하여 자동으로 수정이 가능하다.

이 수정 방법으로 State를 사용하는 법도 배워보자.

State 만들기 및 리렌더링하기

읽기
간단한 앱 하나를 만들어보겠다.

import 'package:flutter/material.dart';

void main() {
  runApp(const App());
}

class App extends StatefulWidget {
  const App({super.key});

  
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Click Here!',
                style: TextStyle(fontSize: 30),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

간단하게 스타일만 적용한 앱이다. Text'Click Here!'를 Stateless하게 렌더링하고 있다. 이제 여기에 State를 넣어서 바뀌게 해보겠다.

State 만들기 및 값 UI에 연동하기

되게 쉽다. State를 사용할 변수를 선언하고, 그 값을 UI 안에 넣어주기만 하면 된다.

class _AppState extends State<App> {
  // 1. State 'counter' 선언
  int counter = 0;

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Click Here!',
                style: TextStyle(fontSize: 30),
              ),
              // 2. 'counter' UI에 표시
              Text(
                '$counter',
                style: const TextStyle(fontSize: 30),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

되게 쉽다! UI에 변수를 쓸 때는 $을 붙이는 걸 잊지 말자.

State 변경 버튼 만들기

2가지가 필요하다. IconButton 등의 버튼 관련 위젯, 그리고 SetState 함수다.

class _AppState extends State<App> {
  int counter = 0;

  void onClicked() {
    counter = counter + 1;
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Click Here!',
                style: TextStyle(fontSize: 30),
              ),
              Text(
                '$counter',
                style: const TextStyle(fontSize: 30),
              ),
              IconButton(
                iconSize: 40,
                onPressed: onClicked,
                icon: const Icon(Icons.add_box_rounded),
              )
            ],
          ),
        ),
      ),
    );
  }
}

되게 쉽다! IconButton을 선언하면 아래와 같이 나온다.

IconButton(onPressed: onPressed, icon: icon)

onPressed에는 Set 함수를 넣고, icon에는 아이콘 관련 데이터를 넣으면 된다!

결과는 아래와 같다.

근데 직접 버튼을 클릭해보면 작동하지 않을 것이다 왜그럴까? JS나 TS에선 저렇게하면 되는데 왜 안되는 걸까?

바로 플러터에는 setState() 메소드를 필수로 사용해야한다.

setState() 메소드

그냥 위 코드와 같이 void onClicked() {counter = counter + 1;}를 넣는다면 플러터에서는 얘가 상태를 바꾸는지 안바꾸는지 모르기 때문이다.

플러터에서 상태가 바뀌고, 그 상태가 바뀌었다면 다시 재렌더링 하라고 플러터에게 명령을 내리는 것과 같다.

void onClicked() {
  setState(() {
    counter = counter + 1;
  });
}

이런 식으로 setState() 메소드를 사용하여 플러터에게 데이터가 변경됐음을 알려주어야 한다.

왜 변경됐음을 알려야하는데?

성능을 최적화하고 불필요한 리렌더링을 방지하기 위해 그렇다.

플러터에서 UI 업데이트는 변경된 상태를 감지하고 필요한 경우에만 다시 렌더링하는, 우리에게 친근한 "리액트 스타일"의 접근 방식을 채택하고 있다. 이를 통해 성능을 최적화하고 불필요한 UI 업데이트를 피할 수 있다.

기본적으로 플러터는 위젯의 build() 메소드가 호출될 때 해당 위젯의 UI를 렌더링한다. 하지만 특정 상태가 변경되었을 때만 UI를 다시 그리는 것이 효율적이다. 이를 위해 setState() 메소드를 사용하여 상태가 변경될 때만 build() 메소드를 호출하고 UI를 업데이트한다.

setState()를 호출하지 않으면 상태 변화가 감지되지 않아 UI 업데이트가 발생하지 않는다. 따라서 setState()를 사용하여 상태 변경을 알려줘야만 플러터는 해당 변경사항을 감지하고 UI를 업데이트한다.


그러한 이유로 setState()를 이렇게도 호출할 수 있다.

void onClicked() {
  counter = counter + 1;
  setState(() {});
}

ㅋㅋㅋ 어떤가! 이 함수를 실행하면 마지막에 리렌더링 해주세요~ 하는 식으로 해도 된다. 리액트를 공부해서인지 이 로직이 이해가 잘갔다 ㅎㅎ

결과

짠! Dart Pad에서 간단히 저렇게 구현할 수 있다! 끝!

profile
코뿔소처럼 저돌적으로

5개의 댓글

comment-user-thumbnail
2023년 5월 31일

state 관련은 비슷해서 새로운 느낌 없이 잘 보이네요 ㅋㅋ 고생하셨습니다 !

답글 달기
comment-user-thumbnail
2023년 6월 3일

저도 리엑트를 해봐서 그런지 이 로직부분은 비교적 이해하기 쉬웠던 것 같아요 !ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 6월 4일

리액트랑 비슷하네요ㅎㅎ 잘 읽었습니다~!

답글 달기
comment-user-thumbnail
2023년 6월 4일

오 진짜 비슷하네용 ㅎㅎ 고생하셨습니당 !

답글 달기
comment-user-thumbnail
2023년 6월 4일

고생하셨습니다 :)

답글 달기