[플러터 인 액션] C3 : 플러터의 세계로 (1) 위젯과 상태 관리

플러터 프로젝트 구조

  • 플러터 프로젝트를 만들면 디렉터리가 생성된다.
  • 모든 디렉터리를 알 필요는 없으며, 사용하지 않는 디렉터리도 있다.
counter_app
=> android 
=> ios
=> lib
	=> main.dart // 프로젝트 진입점, main을 포함
=> test // 비즈니스 로직 검증
	=> widget_test.dart
=> pubspec.yaml // 모든 다트 프로젝트에 필수, 의존성과 메타데이터 관리
=> pubspec.lock // 편집하면 안 되는 잠금 파일 생성 
=> README.md

플러터 앱 내부

  • 플러터의 많은 기능은 다트 라이브러리로 이루어져 있다.
  • 구글의 머티리얼 디자인 시스템 기본 위젯을 사용할 수 있다.

앱 진입점

  • 앱의 맨 윗부분에 main 함수를 선언한다.
  • runApp이라는 메서드로 최상위 위젯을 감싸며, 모든 것은 위젯으로 이루어져 있다.
void main() => runApp(MyApp());

플러터의 위젯

  • 위젯은 다른 프레임워크의 컴포넌트와 같다.
  • 여러 위젯을 사용해 다양한 방법으로 조합하여 더 큰 위젯을 만들어 앱을 완성해야 한다.
  • Button, TextField 같은 위젯은 덜 추상적이며 구조적 요소를 정의한다.
  • 테마 위젯으로 앱의 색과 폰트를 정의하고 위젯으로 애니메이션을 정의한다.
  • 플러터에서는 한 위젯의 스타일로 다른 위젯을 설정한다.
    • 패딩을 추가하기 위해 Padding 위젯을 사용해야 한다.
    • 모든 UI가 위젯이며, 앱의 경로도 위젯으로 이루어져 있다.

build 메서드

  • 모든 위젯은 다른 위젯을 반환하는 build 메서드를 반드시 포함한다.
class RedButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: TextButton(
        child: Text("Red Button"),
        onPressed: () {},
      ),
    ));
  }
}
  • 최상위 위젯은 가장 상위의 위젯이며, 다른 위젯으로 부터 조립된다.
import 'RedButton.dart';

class Myapp extends StatelessWidget {
  // 이 클래스의 슈퍼클래스인 StatelessWidget도 build 
  // 메서드를 갖지만 이 메서드를 대신 호출해야 한다는 의미로 사용된다.
  @override
  Widget build(BuildContext context) {
    // 앱의 모든 위젯에서 머티리얼 디자인 기능을 이용할 수 있도록 앱을 감싼다.
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: RedButton(),
    );
  }
}

new, const 생성자

  • 플러터에서는 한 위젯의 여러 인스턴스를 만든다.
  • 많은 내장 위젯은 일반 생성자와 const 생성자를 모두 제공한다.
  • 가능하면 const를 사용하는 것이 좋으며, 둘 다사용하지 않으면 프레임워크가 const로 추론한다.

핫 리로드

  • 다트는 AOT 컴파일러이면서 JIT 컴파일러다.
  • 컴퓨터로 앱을 개발할 때는 JIT를 사용하며, just in time 컴파일러라고 부른다.
    • 실시간으로 코드를 컴파일하고 실행한다.
  • 배포할 때는 AOT 컴파일러를 사용한다.

위젯 트리와 형식, State 객체

  • 플러터의 대부분의 위젯은 StatelessWidget이나 StatefulWidget의 형식을 가진다.
  • 플러터 UI 개발은 수많은 위젯을 조합해 위젯 트리를 완성하는 것을 뜻한다.

스크린샷 2025-02-18 오후 12.25.56.jpg

  • 위젯은 자신의 자식이 또 다른 위젯들을 포함한다고 설명하는 방식으로 트리를 구축한다.
  • 모든 위젯이 child 프로퍼티를 갖는 것은 아니며, children, builder 등의 프로퍼티를 갖는 위젯도 있다.

상태를 갖지 않는 위젯

  • StatefulWidget은 내부 상태를 추적하며, SatelessWidget은 위젯 생명주기 동안 내부 상태를 가지지 않는다.
class Submitbutton extends StatelessWidget {
  // 위젯으로 전달한 모든 데이터를 설정으로 활용한다.
  late final String buttonText;
  Submitbutton(this.buttonText);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {  },
      child: Text(buttonText)
    );
  }
}
  • 버튼에 대한 텍스트를 다르게 표시할 때, 표시 문자열을 바꾸도록 플러터에 지시할 수 있다.
  • 이는 정적이며, 직접 자신을 갱신하는 로직을 포함하지 않는다.
  • 부모 위젯의 설정에 따라 정해진 문자열을 보여주며, 자신을 언제 어떻게 리빌드해야 하는지 모른다.

상태를 갖는 위젯

  • StatefulWidget 클래스는 Stateless와 다르게 build 메서드를 포함하지 않는다.
  • 대신 상태 객체를 포함하며, 모든 상태 객체가 build 메서드를 포함한다.
class MyHomePage extends StatefulWidget {
  // 슈퍼클래스의 메서드 createState를 오버라이드
  // 모든 StatefulWidget은 State 객체를 반환하는 create 메서드 반드시 정의
  @override
  _MyHomePageState createState() => _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }

}

비공개 클래스

  • _MyHomePageState와 같이 언더바로 시작하는 클래스를 비공개 클래스라고 한다.
  • 현재 클래스에서만 이를 접근한다는 것을 의미한다.

setState

void _incrementCounter() {
  setState((){
    _counter++;
  });
}
  • 이 메서드는 객체의 상태를 변화시키는 기능을 한다.
  • Void-Callback 형식의 인수 하나를 가지고 있다.
  • 비동기 코드는 실행할 수 없다는 특징이 있다.

initState

  • 상태 객체는 위젯이 트리에 마운트되면 호출되는 initState 메서드도 포함한다.
  • 위젯의 build 메서드가 실행되면 화면에 위젯을 그리기 전에 String 등을 알맞은 형태로 포맷한다.
class FirstNameText extends StatefulWidget {
  final String name;

  const FirstNameText({Key? key, required this.name}) : super(key: key);

  @override
  State<FirstNameText> createState() => _FirstNameTextState();
}

class _FirstNameTextState extends State<FirstNameText> {
  late String name;

  @override
  void initState() {
    super.initState();
    name = widget.name.toUpperCase();
  }

  @override
  Widget build(BuildContext context) {
    return Text(name);
  }
}
  • initState는 상태 객체를 만들 때 한 번만 호출된다.
  • setState를 호출해 플러터가 위젯을 다시 그리도록 할 수 있다.

BuildContext

  • 모든 build 메서드는 위젯 트리에서 위젯의 위치를 참조하는 BuildContext 하나를 인수로 받는다.
  • 개발자가 관리할 필요는 없으며, 자주 사용할 수 있다.
  • 모든 위젯은 자신만의 빌드 콘텍스트를 가지며, 한 위젯이 다양한 테마를 변화하게 만들어 한 트리에 여러 테마를 적용할 수 있다. 
    • Theme.of
    • of 메서드는 트리에서 형식이 같은 가장 가까운 부모를 반환한다.

스크린샷 2025-02-18 오후 2.16.22.jpg

  • 특정 위젯을 정확하게 어떻게 표현할지 결정한다.
    • 모달과 라우트 표시에 주로 활용한다.

위젯 활용하기

ElevatedButton

  • 머티리얼 디자인에서 제공하는 버튼 중 하나로, 약간 솟아오른 효과를 제공한다.
  • FlatButton과 달리 레이아웃에 입체적인 느낌을 준다.
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 10;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              // 콜백을 활용해, 부모 위젯에서 상태를 관리한다.
              onPressed: _decrementCounter,
              child: const Text("Decrement Counter"),
            )
          ],
        ),
      ),
    );
  }

  void _decrementCounter() {
    // setState는 콜백을 인수로 받으며, 이 콜백은 위젯의 상태를 갱신한다.
    setState(() => _counter--);
  }
}

상속보다 조합을 선호하는 플러터

  • 상속은 ~은(는) ~이다(is a) 관계를 성립한다.
  • 조합은 갖고 있다(has a) 관계를 성립한다.
  • 조합은 추상 클래스와 비슷하며, 클래스를 만들어 동작을 구현한다.

플러터에서 조합하기

  • 플러터는 항상 상속보다 조합으로 재사용할 수 있고 결합되지 않은 위젯을 만든다.
  • 대부분의 위젯은 자식 위젯이 누구인지 미리 알 수 없다.
import 'package:flutter/material.dart';

class Panicbutton extends StatelessWidget {
  final Widget display;
  final VoidCallback onPressed;
  
  // 표현할 위젯과 위젯 설정을 전달한다.
  Panicbutton({
    required this.display,
    required this.onPressed,
  });

  Widget build(BuildContext context){
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.white,
        foregroundColor: Colors.red,
      ),
      child: display,
      // 콜백을 전달하는데, 어떤 기능과도 의존성이 없어 유연하다.
      onPressed: onPressed,
    );
  }
}
  • 위 코드에서 조합을 이용했으므로 텍스트는 버튼이다가 아닌, 버튼은 텍스트를 포함한다라고 표현할 수 있다.
  • 버튼은 자식이 한 개 있다는 사실만 알 뿐 자식이 무엇인지 알 필요가 없다.