꿈꾸는 시스템 디자이너

Flutter 강좌 - StatelessWidget과 StatefulWidget의 차이점과 사용법 본문

Development/Flutter

Flutter 강좌 - StatelessWidget과 StatefulWidget의 차이점과 사용법

독행소년 2019. 10. 25. 11:09

Flutter Code Examples 강좌를 추천합니다.

  • 제 블로그에서 Flutter Code Examples 프로젝트를 시작합니다.
  • Flutter의 다양한 예제를 소스코드와 실행화면으로 제공합니다.
  • 또한 모든 예제는 Flutter Code Examples 앱을 통해 테스트 가능합니다.

Flutter Code Examples 강좌로 메뉴로 이동

Flutter Code Examples 강좌 목록 페이지로 이동

Flutter Code Examples 앱 설치 | Google Play Store로 이동

 

Flutter Code Examples - Google Play 앱

Are you a beginner at Flutter? Check out the various features of Flutter through the demo. Source code for all demos is also provided.

play.google.com


 

Flutter 강좌 시즌2 목록 : https://here4you.tistory.com/149

 

이번 시간에는 StatelessWidget(이하 SLW)와 StatefulWidget(이하 SFW)의 대해서 알아보고 그 차이점은 무엇인지도 생각해본다.

지난 몇 달간의 강좌가 진행되면서 수많은 StatelessWidget과 StatefulWidget을 사용해왔는데 이제 와서 이들에 대해서 알아본다는 점이 이상할 수도 있으나 본인이 처음에 그랬던 것처럼 두 위젯 간의 차이점에 대해 쉽게 이해할 수 있는 자료를 정리하고 싶어서 이번 강좌를 진행하기로 했다.

SLW와 SFW 모두 UI를 가지는 화면을 구성할 때 사용하는 위젯 클래스다. 두 위젯 모두 Scaffold를 이용해 동일한 방식으로 화면을 구성하게 된다.

 

1. StatelessWidget

우선 화면을 하나 살펴보자.

 

위의 화면은 숫자 0을 출력하는 Text위젯과 두 개의 FloatingActionButton 버튼으로 구성되어 있다. 예상하겠지만 버튼을 누를 때마다 숫자가 1씩 증가하거나 감소하는 기능을 가지는 화면을 구성하고자 한 것이다.

이를 SLW 클래스로 구현하면 그 소스는 다음과 같다.

class SLWdemo extends StatelessWidget {

  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("** build - StatelessWidget Demo");
    return Scaffold(
      appBar: AppBar(title: Text("Stateless Widget")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "$_count",
              style: TextStyle(fontSize: 30),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.add),
                  onPressed: () {
                    _count++;
                    print("value of _count = $_count");
                  },
                ),
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.remove),
                  onPressed: () {
                    _count--;
                    print("value of _count = $_count");
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

코드를 살펴보면,

화면에 출력할 숫자 값을 저장할 _count 변수가 필드로 선언되어 있으며, Text 위젯에서 그 값을 출력하도록 하고 있다. 그리고 숫자를 가감할 각 FloatingActionButton의 onPressed 속성에서 _count의 값을 1씩 증가 혹은 감소시키고 있는 것을 확인할 수 있다. 추가로 값을 가감될 때마다 _count의 값을 print 함수로 출력하게 구현했다.

그럼 실제 버튼을 클릭할 때마다 화면상의 값이 변경될까?

그렇지 않다. 버튼을 클릭하여도 화면상의 숫자의 변화는 발생하지 않는다.

그러나 실행창을 확인하면 다음과 같이 실제 _count의 값은 정상적으로 변경되고 있는 것을 확인할 수 있다.

Performing hot reload...
Syncing files to device Android SDK built for x86...
I/flutter (23923): ** build - StatelessWidget Demo
Reloaded 0 of 468 libraries in 105ms.
I/flutter (23923): value of _count = 1
I/flutter (23923): value of _count = 2
I/flutter (23923): value of _count = 3
I/flutter (23923): value of _count = 2
I/flutter (23923): value of _count = 1

 

다시 말하면, 각 버튼을 클릭하여 해당 버튼의 onPressed 속성의 함수에 의해 _count의 값을 정상적으로 증가하거나 감소되고 있으나 변경된 _count 값이 Text 위젯에서 반영되고 있지 않다는 것을 확인할 수 있다.

이 점이 SLW의 특징이다. 이 특징을 이해하기 위해서는 build 메서드에 대해서 먼저 알아야 한다. build 메서드는 SLW과 SFW(더 정확히는 SFW의 State 클래스)에서 구현되며 화면을 구성할 UI들을 구현하는 메서드다.

즉 화면이 출력될 때 build 메서드가 호출되면서 build 메서드 내부에 구현한 UI 위젯들이 화면에 출력된다는 것이다.

이제 다시 SLW 코드를 살펴보자 build 메소드 내부 첫 번째 줄에 print 함수가 삽입되어 있는 것을 확인할 수 있으며 build 메서드가 호출될 때마다 실행창에 메시지를 출력할 것이다. 그리고 위 실행 창의 세 번째 줄에 메시지가 출력되는 것을 확인할 수 있다. 그런데 5번째 줄부터 내용을 다시 확인해 보면 버튼을 클릭할 때마다 _count 값을 출력하는 print 함수는 호출되고 있지만 build 메서드 첫 번째 줄의 print 함수는 호출되지 않고 있다.

이 점이 화면상에 숫자의 변화가 없는 이유이며 SLW의 특징이기도 하다.

StatelessWidget은 이름 그대로 상태(State)를 가지지 않는 위젯 클래스다. 그래서 SLW 내부의 모든 UI 위젯들은 상태를 가질 수 없으며 상태가 없으니 상태의 변화를 인지할 필요도 없고 할 수도 없는 것이다. 그래서 화면이 생성될 때 한 번만 build 메서드를 호출해서 화면을 구성한 후에는 build 함수가 다시 호출되지 않는다. 버튼을 클릭하여 _count의 값을 변경시키더라도 build 메서드는 호출되지 않으므로 화면 내 Text 위젯의 값도 변경되지 않는 것이다.

SLW을 정리하면,

SLW은 변화가 필요없는 화면을 구성할 때 사용하는 위젯 클래스이며, 그렇기 때문에 build 메서드는 한 번만 호출된다.

 

2. StatefullWidget

SFW은 한번 생성한 화면의 구성이 어떠한 이유로 인해 변경될 수 있는 경우에 사용하는 위젯 클래스다.

SFW의 경우도 다음과 같이 동일한 UI 구성의 화면으로 구현한다.

 

우선 SLW 데모에서 사용한 소스코드를 그대로 이용해서 SFW 데모 코드를 다음과 같이 작성한다.

class SFWdemo extends StatefulWidget {
  @override
  SFWdemoState createState() => SFWdemoState();
}

class SFWdemoState extends State<SFWdemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("** build - StatefulWidget Demo");
    return Scaffold(
      appBar: AppBar(title: Text("Statefull Widget")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "$_count",
              style: TextStyle(fontSize: 30),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.add),
                  onPressed: () {
                    _count++;
                    print("value of _count = $_count");
                  },
                ),
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.remove),
                  onPressed: () {
                    _count--;
                    print("value of _count = $_count");
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

SFW으로 화면을 구성할 때에는 SLW의 경우와는 다르게 StatefulWidget을 상속하는 위젯 클래스와 State를 상속하는 상태 클래스 두 개로 구성된다. 그리고 화면을 구성하는 build 메서드를 SFW 클래스가 아닌 SFW 클래스 타입의 State를 상속하는 상태 클래스에서 구성한다.

왜 이렇게 언어를 개발했는지는 모르겠지만, 매번 하나의 화면을 두개의 클래스로 나눠서 개발하는 점이 번거롭다. 심지어 대부분의 경우 SFW 위젯 클래스는 상태 클래스를 생성시키는 기능만 하는 것이 다인 경우가 많다.

어쨌든 소스 코드로 돌아와서 상태 클래스 내부에 build 메서드의 내용은 위에서 살펴본 SLW 위젯의 소스코드와 일치한다.(AppBar의 텍스트 내용만 다르다.)

그런데 버튼을 클릭해보면 아직까진 화면상의 숫자의 변화는 발생하지 않고 실행 창의 프린트되는 값만 변경되는 것을 확인할 수 있다.

Performing hot reload...
Syncing files to device Android SDK built for x86...
I/flutter (23923): ** build - StatefulWidget Demo
Reloaded 0 of 468 libraries in 110ms.
I/flutter (23923): value of _count = 1
I/flutter (23923): value of _count = 2
I/flutter (23923): value of _count = 3
I/flutter (23923): value of _count = 2
I/flutter (23923): value of _count = 1

 

여기서 알 수 있는 것은, 단순히 StatefulWidget으로 구현한다고만 해서 화면내부 UI가 참조하는 값(속성)의 변화가 화면에 바로 반영되진 않는다는 것이다. 버튼을 클릭하여 _count의 값을 변경하여도 UI는 자신이 참조하는 _count의 값이 변경되었음을 인지하지 못하는다는 것이다. 실제로 실행창에 출력된 값을 보더라도 SLW의 경우와 같이 _count의 값은 변하지만 build 메서드는 호출되고 있지 않고 있는 것을 알 수 있다.

이렇게 변경된 값이 UI 위젯에 반영하기 위해 이용하는 메소드가 setState다.

소스코드를 아래와 같이 변경한다.

class SFWdemo extends StatefulWidget {
  @override
  SFWdemoState createState() => SFWdemoState();
}

class SFWdemoState extends State<SFWdemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("** build - StatefulWidget Demo");
    return Scaffold(
      appBar: AppBar(title: Text("Statefull Widget")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "$_count",
              style: TextStyle(fontSize: 30),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.add),
                  onPressed: () {
                    setState(() {
                      _count++;
                    });
                    print("value of _count = $_count");
                  },
                ),
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.remove),
                  onPressed: () {
                    setState(() {
                      _count--;
                    });
                    print("value of _count = $_count");
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

 

위의 소스코드를 보면 기존의 코드의 각 버튼의 onPressed 속성에서 구현하는 함수의 내용이 조금 바뀌었다. _count 변수의 값을 setState 메서드로 감싼 후 변경하는 것을 알 수 있다.

메서드의 이름에서 알 수 있듯이 setState 메서드는 SFW 내부의 상태를 변경할 때 사용하는 메서드이며 setState 메서드에서 변경된 상태 값을 플랫폼에 전달하여 build 메서드가 호출되도록 한다.

좀 더 자세히 설명하자면,

위 코드에서는 필드인 _count 변수가 이 화면구성의 상태(State)가 된다. 그리고 이 필드는 Text 위젯에서 참조하고 있다. 각 버튼이 클릭될 때 setState 메서드 내부에서 상태 값인 _count의 값을 변경하면, 변경과 동시에 변경 사실이 플랫폼으로 전달되어 build 메서드가 다시 호출되게 되고 Text 위젯은 참조하고 있는 _count의 최신 값을 화면에 출력하게 되는 것이다.

화면을 보면 다음과 같이 클릭하는 버튼에 따라 값이 변경되는 것을 확인할 수 있다.

 

실행창도 확인해 보자.

Performing hot reload...
Syncing files to device Android SDK built for x86...
I/flutter (23923): ** build - StatefulWidget Demo
Reloaded 0 of 468 libraries in 109ms.
I/flutter (23923): value of _count = 1
I/flutter (23923): ** build - StatefulWidget Demo
I/flutter (23923): value of _count = 2
I/flutter (23923): ** build - StatefulWidget Demo
I/flutter (23923): value of _count = 3
I/flutter (23923): ** build - StatefulWidget Demo
I/flutter (23923): value of _count = 2
I/flutter (23923): ** build - StatefulWidget Demo
I/flutter (23923): value of _count = 1
I/flutter (23923): ** build - StatefulWidget Demo

버튼이 클릭될 때마다 _count의 값이 변경되고 있으며 동시에 build 메서드가 호출되는 것을 확인할 수 있다.

 

SFW은 다음과 같이 정리할 수 있다.

  • SFW은 화면의 구성이 상태 변화에 따라 재구성되어야 할 때 사용된다.
  • SFW의 상태 변경은 setState 메서드를 이용해서 변경해야 한다.
  • 플랫폼은 setState 메서드가 호출될 때마다 build 메서드를 재호출하여 화면을 다시 그린다.

 

3. 전체 소스 코드

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: MainPage(),
  ));
}

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("MainPage")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("Launch StatelessWidget"),
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => SLWdemo()));
              },
            ),
            RaisedButton(
              child: Text("Launch StatefullWidget"),
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => SFWdemo()));
              },
            )
          ]
              .map((children) => Container(
                    width: 200,
                    child: children,
                  ))
              .toList(),
        ),
      ),
    );
  }
}

class SLWdemo extends StatelessWidget {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("** build - StatelessWidget Demo");
    return Scaffold(
      appBar: AppBar(title: Text("Stateless Widget")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "$_count",
              style: TextStyle(fontSize: 30),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.add),
                  onPressed: () {
                    _count++;
                    print("value of _count = $_count");
                  },
                ),
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.remove),
                  onPressed: () {
                    _count--;
                    print("value of _count = $_count");
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

class SFWdemo extends StatefulWidget {
  @override
  SFWdemoState createState() => SFWdemoState();
}

class SFWdemoState extends State<SFWdemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("** build - StatefulWidget Demo");
    return Scaffold(
      appBar: AppBar(title: Text("Statefull Widget")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "$_count",
              style: TextStyle(fontSize: 30),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.add),
                  onPressed: () {
                    setState(() {
                      _count++;
                    });
                    print("value of _count = $_count");
                  },
                ),
                FloatingActionButton(
                  heroTag: null,
                  child: Icon(Icons.remove),
                  onPressed: () {
                    setState(() {
                      _count--;
                    });
                    print("value of _count = $_count");
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}
Comments