[Flutter] 위젯

2021-04-07 hit count image

Flutter를 이용하여 앱을 개발해 봅시다. 이번 블로그 포스트에서는 Flutter에서 사용되는 위젯에 관해서 알아봅시다.

개요

Flutter를 사용해서 앱을 개발해 보려고 합니다. 이번 블로그 포스트에서는 Flutter의 기본 요소인 위젯에 관해서 알아보도록 하겠습니다.

이 블로그 포스트에서 소개하는 소스 코드는 아래에 링크에서 확인할 수 있습니다.

위젯

Flutter는 위젯으로 시작해서 위젯으로 끝납니다. Flutter에서 화면에 표시된 모든 요소가 위젯이며, 눈에 보이지 않지만 화면을 구성하는 레이아웃(Layout)도 위젯입니다.

따라서 Flutter로 앱을 개발하기 위해서는 위젯을 이해할 필요가 있습니다.

위젯은 크게 두 가지로 분류할 수 있습니다.

  • Stateful Widget
  • Stateless Widget

그럼 이제 각각의 위젯을 자세히 살펴보도록 합시다.

Stateful Widget

Flutter에서 Stateful Widget은 어떤 상태값을 가지고 있으며, 해당 상태값에 의해 화면에 움직임이나 변화를 표현할 때 사용합니다.

  • 예: 텍스트 필드, 버튼, 서버에서 전달받은 값을 화면에 표시하는 위젯 등

위와 같이 Stateful Widget은 사용자의 인터렉션에 의해 모양이나 형태를 변경할 때 사용합니다. Stateful Widget은 StatefulWidget을 상속받아 생성합니다.

Stateless Widget

Stateless Widget은 Stateful Widget과는 반대로, 어떠한 상태도 가지고 있지 않은 정적인 위젯입니다. 어떠한 상태값도 가지고 있지 않기 때문에 Stateless Widget은 화면에서 어떠한 움직임이나 변화가 없습니다.

  • 예: 텍스트, 이미지 등.

위와 같이 Stateless Widget은 화면에는 표시되지만, 사용자와 어떠한 인터렉션도 하지않으며, 어떠한 움직임이나 변화를 가지고 있지 않습니다. Stateless Widget은 StatelessWidget을 상속하여 생성합니다.

Widget tree

기본적으로 Flutter는 위젯을 사용하여 개발을 하게 됩니다. 한 위젯은 여러 위젯을 포함할 수 있으며, 모든 위젯은 부모-자식 관계를 가지게 됩니다. 이렇게 부모-자식 관계를 가지게 되면, 이를 Tree 계층 구조로 표현할 수 있으며, 관리할 수 있습니다.

예를 들어 설명하면, HTML이 웹 브라우저에 표시될 때, 브라우저는 HTML 요소들을 DOM 트리로 생성하고 관리합니다. 이것과 마찬가지로 Flutter는 모든 위젯을 Widget 트리로 생성하고 관리합니다.

모든 위젯은 부모 위젯과 자식 위젯으로 구성되며 부모 위젯은 Parent Widget이라고도 하지만, Widget Container라고도 합니다.

코딩

그럼 이제 간단하게 코딩을 하면서 위젯을 이해해 보도록 하겠습니다.

Stateless widget 코딩

Stateless widget을 이해하기 위해, 다음 명령어를 사용하여 Flutter의 프로젝트를 생성합니다.

flutter create stateless_widget

그리고 다음 명령어를 사용하여 VSCode를 실행합니다.

cd stateless_widget
code .

이렇게 VSCode가 실행되었다면, lib/main.dart 파일을 열고 다음과 같이 수정합니다.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeWidget(),
    );
  }
}

class HomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stateless Widget'),
      ),
      body: Center(
        child: Text('Hello world'),
      ),
    );
  }
}

그럼 이제 소스 코드를 하나씩 자세히 살펴보도록 하겠습니다.

import 'package:flutter/material.dart';

우선 우리는 Flutter에서 제공하는 material 디자인을 따라 프로그램을 작성할 예정입니다.

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

Dart를 공부하셨다면, main 함수는 친숙할 것입니다. main 함수는 Dart의 프로그램을 시작하기 위한 함수이며, Flutter에서는 이 함수에서 runApp 함수를 호출해야 합니다.

Dart에 관해서 잘 모르신다면, 아래 링크를 통해 Dart에 대해 확인해 보시기 바랍니다.

runApp 함수는 하나의 위젯을 파라메터로 전달받으며, 여기서 전달받은 위젯이 우리가 만들 Flutter 앱을 구성하는 시작점이 되며, 이 위젯은 위젯 트리의 최상단에 위치하게 됩니다.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeWidget(),
    );
  }
}

MyApp은 우리가 만든 커스텀 위젯입니다. 위젯의 이름은 어떤걸 사용해도 되지만, 여기서는 MyApp이라고 지정하였으며, 이 위젯은 StatelessWidget을 상속하였습니다. 즉, 이 위젯은 어떠한 동적 상태값을 가지고 있지 않습니다.

모든 위젯은 기본적으로 build 함수를 가지고 있으며, 이 빌드 함수는 또 다른 위젯을 반환하는 구조를 가지게 됩니다. 이렇게 위젯안에 위젯을 포함하며, 부모-자식 관계가 형성되어, 위젯 트리를 생성하게 됩니다.

이번 예제에서는 flutter/material.dart가 제공하는 MaterialApp 위젯을 사용하여 앱을 구성하였습니다. MaterialApp에는 home이라는 Named 파라메터를 가지고 있습니다.

home은 앱이 실행되고 처음으로 표시되는 화면을 뜻하며, 이 파라메터로 전달된 위젯이 첫 화면에 표시되게 됩니다.

그럼 앱의 첫 화면에 표시되는 HomeWidget을 살펴보도록 합시다. HomeWidget은 MyApp과 같이 StatelessWidget을 상속받아 만든 커스텀 위젯이며, 역시 어떠한 이름을 사용해도 되지만, 여기서는 HomeWidget이라고 지정하였습니다.

class HomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('HOME'),
      ),
      body: Center(
        child: Text('Hello world'),
      ),
    );
  }
}

HomeWidget은 flutter/material.dart가 제공하는 Scaffold라는 위젯을 반환하고 있습니다. Scaffold 위젯은 material 앱에서 기본적으로 사용되는 위젯이며, appBarbody 등과 같이 화면 구성을 쉽게 할 수 있도록 도와주는 위젯입니다.

Scaffold(
  appBar: AppBar(
    title: Text('HOME'),
  ),
  ...,
);

이번 예제에서는 이 Scaffold를 사용하여 앱 상단 바를 표시하였습니다. appBar 파라메터에는 AppBar 위젯을 파라메터로 전달하였으며, AppBar 위젯에는 title 파라메터에 Text 위젯을 사용하여 HOME이라는 글자를 표시하도록 하였습니다.

Scaffold(
  ...,
  body: Center(
    child: Text('Hello world'),
  ),
);

앱의 body에는 Center라는 위젯을 통해, 자식 위젯을 화면 가운데로 정렬 시켰으며, Text 위젯을 통해 Hello world라는 글자를 표시하도록 하였습니다.

Stateful widget 코딩

Stateful widget을 이해하기 위해, 다음 명령어를 사용하여 Flutter의 프로젝트를 생성합니다.

flutter create stateful_widget

그리고 다음 명령어를 사용하여 VSCode를 실행합니다.

cd stateful_widget
code .

이렇게 VSCode가 실행되었다면, lib/main.dart 파일을 열고 다음과 같이 수정합니다.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomeWidget(),
    );
  }
}

class HomeWidget extends StatefulWidget {
  @override
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int counter = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stateful Widget')),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
                onPressed: () => setState(() => {counter++}),
                child: Icon(Icons.add)),
            Container(
              child: Text('$counter'),
              margin: EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 0.0),
            ),
            ElevatedButton(
                onPressed: () => setState(() => {counter--}),
                child: Icon(Icons.remove)),
          ],
        ),
      ),
    );
  }
}

그럼 이제 소스 코드를 하나씩 자세히 살펴보도록 하겠습니다. Stateless Widget에서 설명한 내용은 생략하겠습니다.

그럼 이제 Stateful 위젯을 생성하는 방법에 대해서 살펴보도록 하겠습니다. Stateful 위젯을 생성하기 위해서는 우선 StatefulWidget을 상속 받은 위젯을 생성할 필요가 있습니다.

class HomeWidget extends StatefulWidget {
  @override
  _HomeWidgetState createState() => _HomeWidgetState();
}

Stateless 위젯과는 다르게 build 를 사용하지 않고 createState을 사용해야 하며, createState는 State라는 클래스를 상속받은 커스텀 State를 반환해야 합니다.

여기서 반환되는 커스텀 State는 변경 가능한 상태를 가지고 있으며, 변경되는 상태에 따른 위젯을 가지고 있습니다.

class _HomeWidgetState extends State<HomeWidget> {
  int counter = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
        child: Row(
          ...,
          children: [
            ...,
            Container(
              child: Text('$counter'),
              margin: EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 0.0),
            ),
            ...,
          ],
        ),
      ),
    );
  }
}

우선 변경되는 State를 저장할 변수를 선언합니다. 여기서는 int couter = 0;으로 변수를 선언하였습니다. 이렇게 선언한 변수는 Text 위젯을 사용하여 Text('$counter')을 통해 화면에 표시하도록 하였습니다.

이 State 값을 변경하기 위해서는 setState라는 함수를 사용할 필요가 있습니다. 이 함수를 사용하지 않고 State를 변경할 경우, 화면에 반영되지 않습니다.

ElevatedButton(
  onPressed: () => setState(() => {counter++}),
  child: Icon(Icons.add))

State에 대한 메커니즘은 조금 복잡하므로, 여기서는 사용하는 방법에 대해서만 알아보겠습니다. 메커니즘이 궁금하신 분들은 공식 홈페이지를 참고하시기 바랍니다.

Closing Label

지금까지 예제를 코딩하다보면, 다음과 같이 우리가 추가하지 않았지만, 자동으로 추가되는 주석을 확인할 수 있습니다.

Flutter - Closing Label

Flutter는 수많은 위젯을 중첩해서 앱을 작성하게 됩니다. 이렇게 수많은 위젯과 수많은 코드때문에, 정확히 위젯이 어디서부터 어디까지인지 구별하기가 힘들때가 있습니다. 이를 해결하기 위해서 Flutter는 Closing Label을 사용하여 위젯이 끝나는 부분에 자동으로 주석을 추가하게 됩니다. 이를 통해 우리는 좀 더 쉽게 어떤 위젯이 어디서 시작해서 어디서 끝났는지 쉽게 알 수 있습니다.

이 주석은 우리가 추가할 수도, 수정할 수도 없습니다.

확인

이것으로 간단한 코딩을 통해 Flutter 앱의 화면 구성 방법에 대해서 알아보았습니다. 코드를 확인해 보면 알 수 있듯이, 앱을 구성하는 모든 요소가 위젯인 것을 알 수 있었습니다.

그럼 이제 이렇게 수정한 앱을 실행해 보도록 하겠습니다. 앱을 실행하기 위해 에뮬레이터를 실행하고, Flutter 프로젝트를 실행하는 방법에 대해서는 아래에 링크를 참고하시기 바랍니다.

이렇게 앱을 실행하고 나면 다음과 같이 우리가 만든 앱이 화면에 표시되는 것을 확인할 수 있습니다.

Flutter - First App

완료

이것으로 Flutter를 사용하여 새로운 프로젝트를 생성해 보고, 간단한 예제를 통해 Flutter의 주요 요소인 위젯에 대해서 자세히 살펴보았습니다. Flutter는 이 위젯을 사용하여 모든 앱을 구성하므로, 잘 기억해 두시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
Posts