Stateless and Stateful Widget

오늘은 StatelessWidget과 StatefulWidget에 대해서 배워보도록 하겠습니다.

시작에 앞서 일단 새로운 Flutter앱을 하나 만들게요. Android Studio에서 새로운 Flutter앱을 만드는 방법은 여기를 참고해주세요. 저는 my_first_app이라는 이름으로 앱을 하나 생성했습니다. Flutter는 앱을 새로 만들면 자동으로 데모 앱을 생성해주는데, 이 데모 앱은 화면에 보이는 “+”버튼을 누르면 숫자가 하나씩 더해지는 간단한 앱입니다.

main.dart에 제공된 코드를 보시면요. 우리가 처음에 만들었던 Hello World랑은 다르게 조금 복잡한 모양을 하고 있는데요, 이 구조가 바로 Flutter가 추구하는 이상적인 구조의 코드입니다. 이렇게 만들라고 샘플 코드를 넣어준거니까 어떻게 생겼는지 함께 찬찬히 살펴보고 앞으로 코드를 짤때는 이 구조에 입각해서 코드를 짜도록 하겠습니다.

코드를 줄여놓고 굵직한 것들만 보면 왼쪽의 그림과 같이 main()과 그 밑에 StatelessWidget이 하나 있고 그 밑에 StatefulWidget그리고 마지막으로 State클래스가 있습니다.

이렇게 코드를 4개로 크게 나눈 이유는 main()은 Flutter앱을 실행하면 자동으로 호출하는 함수니까 거기에는 다른 코드는 안넣고 바로 StatelessWidget을 호출하는 걸로 하고, StatelessWidget에서 StatefulWidget을 호출하는데 이렇게 나누어 놓은 이유는 다음과 같습니다.

StatelessWidget에는 변하지 않는 것을 코딩하고, StatefulWidget에는 변하는 것을 코딩하도록 구성했기 때문입니다.

예를 들면, 앱의 이름은 사용자가 앱을 이용하는 동안에 절대 바뀌지 않죠? 그러면 그건 StatelessWidget에 정의를 하고, 버튼을 눌렀을때 화면에 보여지는 카운트는 자꾸 변하니까 StatefulWidget에 정의합니다. 그러면 앱의 바탕색은 어디에 놓을까요? 그건 여러분 마음입니다. 앱의 바탕색을 절대 불변으로 하고 싶으시면 StatelessWidget에 정의를 하시면 되고 중간에 변화를 주고 싶으시면 StatefulWidget에 정의하셔야합니다. 이렇게 두개의 위젯을 구분해놓은 이유는 바로 Performance!! 운영시 효율성때문입니다. 고정된 애들은 구석에 박아 놓고, 갱신될 가능성이 있는 애들은 가까운 곳에 두고 따로 관리하여 바로바로 갱신이 이루어 지도록 하기 위해서 구분을 해놓는 것입니다.

그럼 이제 main.dart 코드의 가장 위에서 부터 함께 차근히 살펴 보도록 하겠습니다.

import 'package:flutter/material.dart';

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

material.dart라이브러리를 import하고 main()함수를 선언하는 부분까지는 이전시간에 배운것과 똑같습니다. 다만 여기서는 main()함수에서 바로 코드를 작성하는 것이 아니라 MyApp()이라는 함수를 따로 선언해서 runApp()에 인자로 넣어줌으로써 main()이 실행되면 자동적으로 MyApp()으로 가도록 설계를 했네요. 그럼 MyApp()을 한번 따라가 볼까요?

아래는 MyApp을 정의해 놓은 StatelessWidget 클래스입니다.

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyApp extends StatelessWidget {
StatelessWidget을 확장하여 MyApp()을 정의한 부분입니다. 편의상 Flutter에서 제공한 주석은 삭제했습니다.

const MyApp({super.key});
클래스 안에 가장 윗쪽에 자기자신을 호출한건 바로 생성자입니다. 생성자는 다른 언어와 마찬가지로 Dart에서도 클래스가 객체화 될때 가장 먼저 딱 한번 실행됩니다. 이때 부모클래스에 정의된 super.key를 함께 호출하는데요. 이 key는요 Widget이 만들어 질때마다 자동으로 생성되는 key인데요. runApp()에 인자로 들어가는 Widget이 위젯 트리의 root가 됩니다. 그리고 이하 다른 위젯들이 그 밑에 트리구조로 붙어서 트리안의 모든 위젯들은 key값을 통해 관리가 되고 key를 통해 각 위젯들을 제어할수도 있습니다.

Widget build(BuildContext context) {
그 다음 함수가 여기서 젤 중요한데요, 일단 함수 위에 @override를 명시함으로써 기존에 부모 클래스에 정의된 build()메서드를 여기서 재정의 하겠다고 알려줍니다. 여기서 정의된 build 위젯은요. StatelessWidget이 실행되면 자동으로 호출되는 메서드입니다. 이 build()에서 반환한 값을 위에서 호출한 runApp()에 전달하기 때문에 얘가 가장 중요한 함수입니다.

return MaterialApp(
지난 시간까지는 main()에서 MaterialApp()을 바로 호출했는데 이번 시간부터는 StatelessWidget의 build()에서 MaterialApp을 호출하도록합니다.

title: 'Flutter Demo',
MaterialApp()의 인자로는 title, theme, 그리고 home이 있는데 그중 title은 이 앱의 이름을 설정하는 부분입니다.
theme: ThemeData(colorScheme: ..,useMaterial3: true,)
여기서 앱의 테마를 정하고 각종 변하지 않는 각종 설정들을 추가해줍니다.
home: const MyHomePage(title: 'Flutter Demo Home Page'),
그리고 가장 중요한 속성인 home에 실제 화면에 들어가는 레이아웃을 만들어 주는 함수를 호출합니다. 그럼 MyHomePage가 어떻게 정의 되었는지 아래 코드를 함께 보겠습니다.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

MyHomePage는 StatefulWidget으로 정의된 클래스입니다. 다시 말해 이 클래스에 정의된 내용들은 앱이 실행된 이후에 각종 설정값들이 변경될 가능성이 있다는 의미입니다.

const MyHomePage({super.key, required this.title});
그리고 이전 클래스와 마찬가지로 key를 가지고 생성자를 호출하는데요. 이때 MyHomePage호출시 인자로 넘겨받은 title도 함께 호출합니다.

State<MyHomePage> createState() => _MyHomePageState();
@override를 명시하여 StatefulWidget에 정의된 createState()을 재정의 하겠다고 명시합니다. 그리고 createState()은 아래 별도로 정의된 _MyHomePageState()클래스를 호출합니다.

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),methods.
    );
  }
}

int _counter = 0;
_MyHomePageState클래스의 내부변수로 _counter를 선언하고 0으로 초기화 합니다.

void _incrementCounter() {
그 밑에 선언된 함수는 호출될때마다 _counter의 값을 하나씩 증가시킵니다.

Widget build(BuildContext context) {
State클래스를 호출하면 자동으로 실행되는 build함수는 마찬가지로 코드를 실행하여 생성된 화면 레이아웃을 StatefulWidget에 전달합니다.

return Scaffold(appBar: .., body: .., floatingActionButton: ..)
Scaffold를 이용해서 화면을 구성합니다. Scaffold의 appBar속성은 앱의 최상단에 보여지는 해당 페이지의 대표제목입니다. backgroundColor와 title을 정의하여 Flutter Demo Home Page라는 타이틀을 화면에 보여줍니다.

body: Center(child: Column(..))
body로는 일단 Center()를 호출하여 가운데 정렬을 하도록 하고, child에는 Column을 넣어 리스트 형태로 컨텐츠가 들어가게 합니다. Column의 children속성에 위젯 배열을 선언하여 보여주고자 하는 메세지 Text()와 카운트번호를 담을 Text()를 선언해줍니다. 첫번째 Text()는 고정된 글씨이므로 const를 넣어 선언하고, 카운터는 text 값에 '$_counter'를 넣어 값이 변하는 대로 화면에도 갱신되도록 해줍니다.

floatingActionButton: FloatingActionButton(..)
마지막 라인은 이전시간에 설명했으므로 생략하도록 하겠습니다.

위에서 설명드린대로 데모앱을 실행을 하면 StatelessWidget에 정의된것은 구석에, StatefulWidget에 정의된것은 근처에 둠으로써 운영상의 효율을 극대화 합니다. 눈에 보이지는 않지만 이렇게 고정된 레이아웃과 갱신이 되는 레이아웃을 나눠 놓음으로써 갱신시 속도가 매우 빠르고 메모리 효율도 좋은 앱을 만들수 있습니다.

오늘 강의를 통해서 StatelessWidget과 StatefulWidget의 차이를 확실히 아셨으리라 생각됩니다. 다음 시간에는 Stateless와 Stateful 위젯을 사용했을때 코딩속도가 빨라지는 Hot Reload와 Hot Reset에 대해서 공부해보도록 하겠습니다.

References