Flutter Fundamentals

조이·2025년 11월 6일

플러터

목록 보기
1/1

Learn the fundamentals

https://docs.flutter.dev/get-started/fundamentals

플러터 핵심에 관한 공식 문서를 읽고 배운 점을 요약해본다. 공부 목적은 아래 질문을 답하기 위해서이다. (아직 5,6번은 해결 덜 됨)

  1. build()는 어떤 의미일까?
  2. State는 왜 별도 클래스로 선언할까?
  3. constraints는 뭘 가리키는 걸까?
  4. build()와 setState(), 그리고 무한루프
  5. 위젯의 생명주기
  6. MVVM, Callback Notifier, Inherited Widget 이 셋의 연결고리

Dart

  • 다트: 플러터 어플리케이션, 위젯은 모두 다트로 쓰여있으므로, 어느 정도의 문법은 이해 필요

Widgets

https://docs.flutter.dev/get-started/fundamentals/widgets
https://docs.flutter.dev/get-started/fundamentals/layout#understanding-layout-in-flutter

  • 위젯: "Everything is a widget (in Flutter)". 플러터의 구성(composition) 단위. 위젯은 다시 하나 또는 여러 개의 위젯으로 구성될 수 있다.
  • 위젯은 레이아웃 위젯, 기능(utility) 위젯, 시각적 표현(visual representation) 위젯 등으로 나뉜다.
    • 레이아웃 위젯: Padding, Alignment, ... 시각적 표현은 없고 위젯들의 정렬(layout)을 통제함
    • utility 위젯: Container. 정렬(layout), painting, positioning, sizing 등 여러 가지를 결정하는 위젯의 합.
    • visual representation 위젯: ElevatedButton 등.
  • build() 메서드: 모든 위젯은 자기 객체에 build 메서드를 가지고 있어야하며(override), 이 build 메서드는 또 다른 위젯을 반환해야한다.
    • 프레임워크는 위젯이 생성되거나, 위젯의 의존성이 바뀌면(전달된 상태가 바뀐다던지) build 메서드를 호출한다.
    • build 메서드는 모든 프레임에 불릴 수 있으며 (potentailly be called), 위젯을 만드는(build) 것 외의 부수효과(side-effect)를 가지면 안 된다.

Layouts

https://docs.flutter.dev/get-started/fundamentals/layout

플러터가 UI 툴킷이라는 점을 고려했을 때, 플러터 개발자는 대부분 위젯으로 레이아웃을 만드는 데 시간을 쓰게 된다. 특히 가장 흔한 에러 중 하나인 "unbounded constraints" 에러도 자주 만나게 될 것이다.

  • constraints(제약조건)
    • "레이아웃"은 기본적으로 위젯의 크기와 (화면상의) 위치를 정하는 것을 의미한다.
    • 위젯의 크기와 위치는 부모와의 대화(conversation)에 의해 정해진다(constrained by).
      1. 위젯은 부모로부터 4개의 double값(min/max width/height)으로 이루어진 제약조건(constrains)을 받는다.
      2. 위젯은 이 사이즈 내에서 자기 사이즈와 어떻게 정렬되고 싶은지(align)를 정해 부모에게 돌려준다.
      3. 부모는 이 값을 보고, 이 위젯을 어떻게 위치시킬지(position) 결정한다.
      4. 정렬(alignment)은 외부에서 정해질 수도 있다. (예: Center, Row/Column의 alignment 프로퍼티 등)
    • 플러터에서는 이 대화를 이렇게 부르곤 한다: "Constraints go down. Size go up. Parent sets the position."

TBC... https://docs.flutter.dev/get-started/fundamentals/layout#box-types

State

https://docs.flutter.dev/get-started/fundamentals/widgets#widget-state
https://docs.flutter.dev/get-started/fundamentals/state-management

아래에서는 상태 관리, 즉 앱 안에서 이런 정보 자원들을 어떻게 관리할지 설계하기 위해서 사용할 수 있는 방법을 크게 3가지로 나누어 다룬다:

  1. 상태를 위젯 내에서 직접 관리하는 방법
  2. 위젯 간 상태를 공유하는 방법
  3. 상태가 '바뀌었다'는 사실만 전달하는 방법

정의 - 상태, 그리고 상태관리

  • 상태: 플러터 앱이 UI나 시스템 자원을 관리하기 위해 사용하는 모든 객체를 가리킨다.
  • 상태관리: 이런 객체들을 위젯간에 쉽게 공유하고 접근하기 위해 앱을 구성하는 방법을 가리킨다.

1. 상태를 위젯 내에서 직접 관리하는 방법

a. StatefulWidget

상태를 관리하는 가장 간단한 방법. 상태를 위젯 안에 저장하는 방식.

  • 플러터에는 두 가지 주요 위젯 클래스가 있다: stateful 위젯, stateless 위젯.
  • stateless 위젯
    • : 변경되는(mutable) 상태(클래스 프로퍼티)가 없는 위젯
    • 다수의 built-in 위젯은 stateless 이다. Padding, Text, Icon 등.
    • 당신이 만드는 위젯의 대부분은 stateless 위젯일 것이다.
  • stateful 위젯
    • : 상태가 바뀔 수 있고, 그에 따라 UI를 재빌드하는 위젯.
  • StatefulWidget / State
    • 위젯 자체는 immutable하고, StatefulWidget을 상속한다. 그리고 mutable한 상태 클래스를 가진다. 이 상태 클래스는 State를 상속한다.
    • StatefulWidget은 build 메서드가 없고, State가 build 메서드를 가진다.
    • State 객체를 변경할 때는 반드시 setState() 메서드를 호출해서 프레임워크에 신호(signal)를 보내야한다. 그러면 프레임워크는 State의 build() 메서드를 재호출하고, UI를 업데이트한다.
    • State를 분리함으로써, 위젯 밖에서는 해당 위젯이 stateless인지 stateful인지 신경쓰지 않아도 된다. 자식의 상태가 잘 보존되는지 걱정할 필요 없이, 부모는 자식의 인스턴스를 자유롭게 만들 수 있다. 상태를 찾고 재사용하는 작업은 프레임워크가 다 해결해준다.
    • StatefulWidgetState
      변경(mutable)XO
      build()XO

예시 코드

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

  
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int count = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        TextButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text('Increment'),
        )
      ],
    );
  }
}

위 예시는 아래 두 가지 컨셉을 충족한다:

  1. 캡슐화(Encapsulation): MyCountercount 변수에 대해 몰라도 된다
  2. 객체 생애주기(Object lifecycle): _MyCounterState 객체와 그 안의 count 변수는, MyCounter 가 처음 빌드될 때 생성되었다가 MyCounter 가 화면에서 사라질 때 제거된다. (임시 상태 - "ephemeral state"라고 부른다. ephemeral 은 수명이 짧다는 뜻인데 한글권에서는 주로 임시라고 번역하는 듯.)

2. 위젯 간 상태를 공유하는 방법

a. 위젯 생성자 (a.k.a. "prop drilling")

  • 다트에서 객체는 레퍼런스로 전달되기 때문에 (pass by reference), 생성자에서 바로 넘길 정보값들을 담은 객체를 선언하는 것이 흔하다.
  • 객체를 생성할 때 어떤 정보를 넘겨야하는지 명확히 보이는 장점이 있다. 객체가 의존하는 정보를 '주입'하기 때문에 dependency injection 패턴이라고 볼 수 있다.
  • 단, 이 정보들을 위젯트리를 따라 내려줘야할 때는 코드가 길어지고(verbose) 불필요한 보일러플레이트 코드가 많아지는 단점이 있다.

b. InhertiedWidget 사용 (혹은 "provider" 등 유사한 패키지)

  • 부모에 데이터를 저장하고, 자식은 필드로 저장할 필요 없이 접근 가능.
  1. InheritedWidget 을 상속하고 static 메서드인 of()updateShouldNotify() 를 구현한다.
  2. 그리고 이 상태를 사용할 위젯의 build 메서드에서 of() 호출.
  3. updateShouldNotify() 를 통해 새 데이터를 비교하고,
  4. 반환값이 true면 해당 위젯 리빌드.

테마 등이 이 InheritedWidget의 대표적인 예시.

c. 콜백 호출 (부모에게 변화 전달)

  • 플러터에서 제공하는 ValueChanged<T> 타입 사용: typedef ValueChanged<T> = void Function(T value);
class MyState extends InheritedWidget {
  const MyState({
    super.key,
    required this.data,
    required super.child,
  });

  final String data;

  static MyState of(BuildContext context) {
    // This method looks for the nearest `MyState` widget ancestor.
    final result = context.dependOnInheritedWidgetOfExactType<MyState>();

    assert(result != null, 'No MyState found in context');

    return result!;
  }

  
  // This method should return true if the old widget's data is different
  // from this widget's data. If true, any widgets that depend on this widget
  // by calling `of()` will be re-built.
  bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}

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

  
  Widget build(BuildContext context) {
    var data = MyState.of(context).data;
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

3. 상태가 '바뀌었다'는 사실만 전달하는 방법

플러터는 Listenable라는 추상 클래스를 지원한다. Listenable은 한 개 이상의 리스너들을 업데이트할 수 있다. Listenable 을 사용하는 방법 중 대표적인 몇 가지를 소개하자면 (Listenable을 구현하는 클래스들):

  1. ChangeNotifier를 선언하고 ListenableBuilder로 이 Notifier를 구독한다.
  2. ValueNotifierValueListenableBuilder를 사용한다.

a. ChangeNotifier

  1. ChangeNotifier 를 상속하는 클래스를 만들고, 상태가 변경되는 곳에서(변경 되었다고 알려야할 때) notifyListeners() 를 호출한다.
  2. ListenableBuilder 에 위 객체를 전달하고, builder 함수에서 서브트리를 생성한다.
  3. Notifier 에서 변경이 있을 때마다 Listenable 쪽의 서브트리가 리빌드될 것이다.
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

Column(
  children: [
    ListenableBuilder(
      listenable: counterNotifier,
      builder: (context, child) {
        return Text('counter: ${counterNotifier.count}');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.increment();
      },
    ),
  ],
)

b. ValueNotifier

ChangeNotifier 을 상속하는 클래스로, 더 심플한 버전. 값을 하나만 저장한다 (single value).

Then use the value field to read or update the value, and notify any listeners that the value has changed. Because ValueNotifier extends ChangeNotifier, it is also a Listenable and can be used with a ListenableBuilder. But you can also use ValueListenableBuilder, which provides the value in the builder callback:

  1. ValueNotifier 인스턴스를 생성한다.
  2. ValueListenableBuilder 에 위 객체를 전달하고, value 필드를 이용해 값을 읽거나 업데이트한다.

ValueNotifierChangeNotifier 상속한 것이므로, ValueListenableBuilder 대신 ListenableBuilder 를 사용할 수도 있다. 다만 ValueListenableBuilderbuilder() 함수에서 value 인자를 넘겨주므로 더 편리한 점이 있다.

ValueNotifier<int> counterNotifier = ValueNotifier(0);

Column(
  children: [
    ValueListenableBuilder(
      valueListenable: counterNotifier,
      builder: (context, value, child) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

InheritedWidget vs. Listenable

InheritedWidget 을 직접 쓰면:

  • 최신 상태를 InheritedWidget 이 들고 있음
  • rebuild가 필요한 자식 위젯들은 of(context) 로 접근
  • 상태가 바뀌면 InheritedWidget 자체를 새로운 인스턴스로 교체해야 함 → 그걸 감지한 위젯들이 재빌드

즉, 상태가 바뀌면 전체 InheritedWidget 이 rebuild 되어야 하고, 상태를 바꿀 때마다 항상 새 위젯 생성. 그래서 상태 변경이 자주 일어나는 경우 매우 번거롭고 boilerplate가 많아짐.

Listenable:

Listenable을 듣고 있는 객체만 리빌드됨. 즉 Notifier+Listenable은 값이 바뀌었다는 사실만 전달, 값 자체를 전달하지는 않음.
상태를 클래스로 묶고(ChangeNotifier), 그 상태가 바뀌는 걸 notifyListeners 로 알리면 → UI 코드가 상태 클래스에 덜 의존하고 깔끔해짐. InheritedWidget만 쓰면 상태 변경 로직이 UI 쪽에 더 퍼지기 십상.

MVVM

https://docs.flutter.dev/get-started/fundamentals/state-management#using-mvvm-for-your-applications-architecture

모델

  • 로우레벨의 할일들을 정의한 다트 클래스. (HTTP 요청, 데이터 캐싱, 시스템 자원 관리 등)
  • 대부분 '플러터' 라이브러리가 필요하지 않다. 플러터 primitive나 그 외 플랫폼에 의존하지 않는다. (platform-agnostic)
  • 덕분에 유닛테스트에서 Mock 으로 대체될 수 있고, 로우레벨
import 'package:http/http.dart';

class CounterData {
  CounterData(this.count);

  final int count;
}

class CounterModel {
  Future<CounterData> loadCountFromServer() async {
    final uri = Uri.parse('https://myfluttercounterapp.net/count');
    final response = await get(uri);

    if (response.statusCode != 200) {
      throw ('Failed to update resource');
    }

    return CounterData(int.parse(response.body));
  }

  Future<CounterData> updateCountOnServer(int newCount) async {
    // ...
  }
}

0개의 댓글