개요

Flutter에서 setState는 가장 기본적인 상태 관리 방법입니다. StatefulWidget의 상태를 변경하고 UI를 다시 그리도록 하는 메커니즘을 제공합니다. 이번 포스트에서는 setState의 개념과 사용법에 대해 자세히 알아보겠습니다.

 

 

setState의 기본 개념

 

StatefulWidget과 State

// StatefulWidget 예시
class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;  // 상태 변수

  @override
  Widget build(BuildContext context) {
    return Text('Count: $_count');
  }
}

 

 

setState 기본 사용법

class _CounterState extends State<Counter> {
  int _count = 0;

  void _incrementCounter() {
    setState(() {
      _count++;  // 상태 변경
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

 

 

setState의 동작 원리

 

State 생명주기

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // 초기화 코드
  }

  @override
  void dispose() {
    // 정리 코드
    super.dispose();
  }

  @override
  void setState(VoidCallback fn) {
    super.setState(fn);
    // setState가 호출되면 build 메서드가 다시 실행됨
  }

  @override
  Widget build(BuildContext context) {
    // UI 구성
    return Container();
  }
}

 

 

setState 호출 시 일어나는 일

  1. setState 함수 호출
  2. 상태 업데이트
  3. build 메서드 재실행
  4. 위젯 트리 재구성
  5. UI 업데이트

 

 

실전 활용 예시

 

기본적인 카운터 앱

class CounterApp extends StatefulWidget {
  const CounterApp({Key? key}) : super(key: key);

  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _counter = 0;

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

  void _decrementCounter() {
    setState(() {
      if (_counter > 0) {
        _counter--;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter App')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Count:',
              style: TextStyle(fontSize: 20),
            ),
            Text(
              '$_counter',
              style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrementCounter,
                  child: Icon(Icons.remove),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _incrementCounter,
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

 

폼 데이터 관리

class UserForm extends StatefulWidget {
  @override
  _UserFormState createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
  String _name = '';
  String _email = '';
  bool _subscribed = false;

  void _updateName(String value) {
    setState(() {
      _name = value;
    });
  }

  void _updateEmail(String value) {
    setState(() {
      _email = value;
    });
  }

  void _toggleSubscription(bool? value) {
    setState(() {
      _subscribed = value ?? false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: [
          TextField(
            onChanged: _updateName,
            decoration: InputDecoration(
              labelText: 'Name',
            ),
          ),
          TextField(
            onChanged: _updateEmail,
            decoration: InputDecoration(
              labelText: 'Email',
            ),
          ),
          CheckboxListTile(
            title: Text('Subscribe to newsletter'),
            value: _subscribed,
            onChanged: _toggleSubscription,
          ),
          ElevatedButton(
            onPressed: () {
              // 폼 제출 로직
              print('Name: $_name');
              print('Email: $_email');
              print('Subscribed: $_subscribed');
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

 

 

리스트 관리

class TodoList extends StatefulWidget {
  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final List<String> _todos = [];
  final TextEditingController _controller = TextEditingController();

  void _addTodo() {
    if (_controller.text.isNotEmpty) {
      setState(() {
        _todos.add(_controller.text);
        _controller.clear();
      });
    }
  }

  void _removeTodo(int index) {
    setState(() {
      _todos.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _controller,
                  decoration: InputDecoration(
                    hintText: 'Enter new todo',
                  ),
                ),
              ),
              IconButton(
                icon: Icon(Icons.add),
                onPressed: _addTodo,
              ),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _todos.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(_todos[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () => _removeTodo(index),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

 

 

setState 사용 시 주의사항

 

비동기 작업에서의 setState

class _MyWidgetState extends State<MyWidget> {
  String _data = '';

  // 잘못된 사용
  void _fetchData() async {
    final response = await API.getData();
    if (!mounted) return;  // 위젯이 여전히 트리에 있는지 확인
    setState(() {
      _data = response;
    });
  }
}

 

성능 고려사항

// 비효율적인 방법
setState(() {
  for (var i = 0; i < 1000; i++) {
    // 상태 변경
  }
});

// 효율적인 방법
void _updateMultipleStates() {
  final updates = List.generate(1000, (index) => index);
  setState(() {
    // 한 번에 모든 상태 업데이트
    _items.addAll(updates);
  });
}

 

setState의 장단점

장점

  • 사용이 간단하고 직관적
  • 작은 규모의 앱에서 효과적
  • Flutter의 기본 제공 기능으로 추가 패키지 불필요

단점

  • 복잡한 상태 관리에는 적합하지 않음
  • 위젯 트리가 깊어지면 상태 전달이 어려움
  • 전역 상태 관리에는 부적합

 

 

마치며

setState는 Flutter에서 가장 기본적인 상태 관리 방식입니다. 작은 규모의 앱이나 로컬 상태 관리에는 매우 효과적이지만, 앱이 커지고 복잡해질수록 다른 상태 관리 솔루션을 고려해야 할 수 있습니다. 다음 포스트에서는 Provider나 Riverpod와 같은 더 강력한 상태 관리 솔루션에 대해 알아보도록 하겠습니다.

'Language > Flutter' 카테고리의 다른 글

[Flutter] 위젯 생명주기(Lifecycle)  (0) 2024.11.20
[Dart] fold() 메소드  (0) 2024.11.19
[Dart] Spread 연산자(...)  (0) 2024.11.17
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15

개요

Spread 연산자(...)는 Dart 2.3 버전에서 도입된 기능으로, 컬렉션의 요소들을 다른 컬렉션에 편리하게 삽입할 수 있게 해주는 연산자입니다. 이번 포스트에서는 Spread 연산자의 다양한 사용법과 활용 사례에 대해 알아보겠습니다.

 

 

 

Spread 연산자

 

List에서의 사용

void main() {
  var list1 = [1, 2, 3];
  var list2 = [4, 5, 6];
  
  // Spread 연산자를 사용하여 리스트 병합
  var combined = [...list1, ...list2];
  print(combined);  // [1, 2, 3, 4, 5, 6]
  
  // 개별 요소와 함께 사용
  var withMore = [0, ...list1, 4];
  print(withMore);  // [0, 1, 2, 3, 4]
}

 

Set에서의 사용

void main() {
  var set1 = {1, 2, 3};
  var set2 = {3, 4, 5};
  
  // Set 병합 (중복제거)
  var combinedSet = {...set1, ...set2};
  print(combinedSet);  // {1, 2, 3, 4, 5}
}

 

 

Map에서의 사용

void main() {
  var map1 = {'a': 1, 'b': 2};
  var map2 = {'c': 3, 'd': 4};
  
  // Map 병합
  var combinedMap = {...map1, ...map2};
  print(combinedMap);  // {a: 1, b: 2, c: 3, d: 4}
  
  // 충돌이 있는 경우 나중에 오는 값이 우선
  var map3 = {'a': 5, 'e': 6};
  var overrideMap = {...map1, ...map3};
  print(overrideMap);  // {a: 5, b: 2, e: 6}
}

 

 

Null-aware Spread 연산자 (...?)

void main() {
  List<int>? nullableList;
  var list = [1, 2, ...?nullableList];
  print(list);  // [1, 2]
  
  nullableList = [3, 4];
  list = [1, 2, ...?nullableList];
  print(list);  // [1, 2, 3, 4]
}

 

조건부 사용

void main() {
  var items = [1, 2];
  List<int>? extraItems;
  
  // null이 아닐 때만 spread
  var combined = [
    ...items,
    if (extraItems != null) ...extraItems,
  ];
  print(combined);  // [1, 2]
}

 

 

 

Spread 활용

 

Flutter 위젯 트리에서의 활용

class MyWidget extends StatelessWidget {
  final List<Widget>? optionalWidgets;
  
  const MyWidget({Key? key, this.optionalWidgets}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Fixed Widget'),
        ...?optionalWidgets,
        Text('Another Fixed Widget'),
      ],
    );
  }
}

 

데이터 모델 복사 및 업데이트

class User {
  final String name;
  final Map<String, dynamic> preferences;
  
  User(this.name, this.preferences);
  
  User copyWith({
    String? name,
    Map<String, dynamic>? extraPreferences,
  }) {
    return User(
      name ?? this.name,
      {
        ...preferences,
        if (extraPreferences != null) ...extraPreferences,
      },
    );
  }
}

 

API 응답 병합

Future<Map<String, dynamic>> fetchUserData() async {
  final basicInfo = await fetchBasicInfo();
  final preferences = await fetchPreferences();
  final settings = await fetchSettings();
  
  return {
    ...basicInfo,
    'preferences': {...preferences},
    'settings': {...settings},
  };
}

 

 

더 자세히

중첩된 컬렉션

void main() {
  var nested = [
    [1, 2],
    [3, 4],
    [5, 6],
  ];
  
  // 중첩 리스트 펼치기
  var flattened = [
    for (var list in nested) ...list,
  ];
  print(flattened);  // [1, 2, 3, 4, 5, 6]
}

 

메모리 사용

// 새로운 리스트 생성 - 메모리 사용
var newList = [...oldList];  // 전체 복사

// 참조만 생성 - 메모리 효율적
var reference = oldList;     // 참조만 복사

 

 

대규모 컬렉션 처리

void processLargeCollections() {
  var largeList1 = List.generate(10000, (i) => i);
  var largeList2 = List.generate(10000, (i) => i + 10000);
  
  // 메모리 사용량이 큼
  var combined = [...largeList1, ...largeList2];
  
  // 대안: Iterator 사용
  var iterator = Chain([largeList1.iterator, largeList2.iterator]);
}

 

 

주의사항

 

타입 안전성

void main() {
  List<int> numbers = [1, 2, 3];
  List<String> strings = ['a', 'b', 'c'];
  
  // 컴파일 에러: 타입 불일치
  // List<int> combined = [...numbers, ...strings];
  
  // 올바른 방법: dynamic 또는 Object 사용
  List<dynamic> combined = [...numbers, ...strings];
}

 

 

Const 컨텍스트

// const 컨텍스트에서 사용 가능
const list1 = [1, 2, 3];
const list2 = [4, 5, 6];
const combined = [...list1, ...list2];  // OK

// 런타임 값에는 const 사용 불가
var runtime = [1, 2, 3];
const invalid = [...runtime];  // Error

 

 

 

결론

Spread 연산자는 컬렉션을 다룰 때 매우 유용한 도구입니다. 특히 Flutter 개발에서 위젯 트리를 구성할 때 자주 사용되며, 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 다만, 대규모 컬렉션을 다룰 때는 메모리 사용량을 고려해야 하며, 타입 안전성에도 주의를 기울여야 합니다.

'Language > Flutter' 카테고리의 다른 글

[Dart] fold() 메소드  (0) 2024.11.19
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Null Safety  (0) 2024.11.16
[Dart] var, dynamic, final, late, const 키워드  (0) 2024.11.15
[Flutter] Widget의 크기제약  (0) 2024.11.07

개요

Dart 2.12 버전부터 도입된 Null Safety는 널 참조로 인한 런타임 에러를 방지하기 위한 기능입니다. 이를 통해 개발자는 컴파일 타임에 잠재적인 널 참조 오류를 발견하고 수정할 수 있습니다. 바로 들어가봅시다.
 
 

Non-nullable 타입

기본적으로 모든 타입은 null을 허용하지 않습니다.

String name = 'John';     // OK
int age = 25;            // OK
String name = null;      // Error
int age = null;         // Error

 

Nullable 타입

타입 뒤에 ?를 붙여 null을 허용할 수 있습니다.

String? nullableName = 'John';  // OK
nullableName = null;           // OK

int? nullableAge = 25;         // OK
nullableAge = null;           // OK

 
 

Null 검사 연산자

null 인식 연산자 (?)

객체가 null일 수 있는 경우 안전하게 접근하기 위해 사용합니다.

String? name = 'John';
print(name?.length);    // name이 null이 아니면 길이 출력, null이면 null 출력

String? nullName = null;
print(nullName?.length);  // null 출력

// 체이닝
class User {
  String? email;
}

User? user = User();
print(user?.email?.length);  // email이 null이면 null 출력

 
 

null 주장 연산자 (!)

nullable 타입을 non-nullable 타입으로 강제 변환합니다. null일 경우 런타임 에러가 발생하므로 주의해서 사용해야 합니다.

String? nullableName = 'John';
String nonNullableName = nullableName!;  // OK

String? nullName = null;
String nonNullName = nullName!;  // Runtime Error

 
 

null 병합 연산자 (??)

왼쪽 피연산자가 null인 경우 오른쪽 피연산자를 반환합니다.

String? name = null;
String displayName = name ?? 'Anonymous';  // 'Anonymous' 출력

String? firstName = null;
String? middleName = null;
String? lastName = 'Doe';
String displayName = firstName ?? middleName ?? lastName ?? 'Anonymous';  // 'Doe' 출력

 
 

late 키워드와 Null Safety

late 변수 선언

초기화를 나중에 하는 non-nullable 변수를 선언할 수 있습니다.

class User {
  late String email;  // 나중에 초기화할 non-nullable 변수
  
  void init(String userEmail) {
    email = userEmail;  // 초기화
  }
}

 
 

late final 변수

한 번만 초기화할 수 있는 변수를 선언합니다.

class User {
  late final String email;
  
  void init(String userEmail) {
    email = userEmail;  // 첫 번째 초기화 OK
    email = 'new@email.com';  // Error: final 변수는 재할당 불가
  }
}

 
 

주의사항

null 주장 연산자(!) 사용 시 주의사항

// 위험한 코드
void processUser(User? user) {
  print(user!.name);  // user가 null이면 런타임 에러
}

// 안전한 코드
void processUser(User? user) {
  if (user == null) return;
  print(user.name);  // null 체크 후 안전하게 접근
}

 
조건부 속성 접근

class User {
  final String? email;
  
  User({this.email});
  
  bool get hasEmail => email?.isNotEmpty == true;
  
  String get displayEmail => email ?? 'No email provided';
}

 
컬렉션에서의 Null Safety

// 리스트 요소가 null일 수 있는 경우
List<String?> nullableStrings = ['Hello', null, 'World'];

// 리스트 자체가 null일 수 있는 경우
List<String>? nullableList = null;

// 둘 다 null일 수 있는 경우
List<String?>? bothNullable = null;

// null이 아닌 요소만 처리
void processStrings(List<String?> strings) {
  for (final string in strings) {
    if (string != null) {
      print(string.toUpperCase());
    }
  }
}

 
 

결론

Null Safety는 코드의 안정성을 크게 향상시키는 중요한 기능입니다. 적절히 사용하면 널 참조 오류를 미리 방지하고, 코드의 의도를 더 명확하게 표현할 수 있습니다. 특히 Flutter 애플리케이션 개발에서는 필수적인 기능이므로, 잘 이해하고 활용하시기 바랍니다.

개요

Dart에서는 변수를 선언할 때 다양한 키워드를 사용할 수 있습니다. 각 키워드는 서로 다른 특성과 용도를 가지고 있어, 상황에 맞게 적절한 키워드를 선택하는 것이 중요합니다. 이번 글에서는 각 키워드의 특징과 사용법에 대해 자세히 알아보겠습니다.
 
 
 

var

var는 타입 추론을 사용하는 변수 선언 방식입니다. 변수를 초기화할 때 할당되는 값의 타입에 따라 자동으로 변수의 타입이 결정됩니다.
 

void main() {
  var name = 'John';     // String으로 추론
  var age = 25;          // int로 추론
  var height = 175.5;    // double로 추론
  var isStudent = true;  // bool로 추론
  var numbers = [1,2,3]; // List<int>로 추론
}

 
 

특징

한번 추론된 타입은 변경할 수 없습니다.

void main() {
    var name = 'Jone';
    name = 'Jane';
    
    name = 10; // ERROR !!
}

 
 
초기값을 지정하지않으면 dynamic 타입이 됩니다.

void main() {
    var name;
    name = 'Jane';
    
    name = 10; // OK
}

 
 
 

dynamic

dynamic은 모든 타입의 값을 저장할 수 있는 특별한 타입입니다. 런타임에 타입 체크가 이루어집니다.
 

void main() {
  dynamic value = 'Hello';
  print(value.runtimeType);  // String
  
  value = 42;
  print(value.runtimeType);  // int
  
  value = true;
  print(value.runtimeType);  // bool
}

 
 

특징

타입에 제약을 받지않고 할당할 수 있습니다.

dynamic value = 'Hello';
value = 42;        // OK
value = true;      // OK
value = [1,2,3];   // OK

 
다만, 런타임 에러의 위험이 있습니다.

dynamic value = 42;
print(value.length);  // Runtime Error

 
주로 JSON 파싱과 같이 타입이 동적으로 결정되는 경우 사용됩니다.
 
 
 

final

final은 한 번만 값을 할당할 수 있는 불변 변수를 선언할 때 사용합니다.

void main() {
  final String name = 'John';
  final age = 25;  // 타입 추론 가능
  
  name = 'Jane';  // Error
}

 
 

특징

런타임에 값이 결정될 수 있습니다.

final time = DateTime.now();  // OK
final random = Random().nextInt(100);  // OK

 
클래스의 인스턴스 변수로 사용 가능합니다.

class Person {
  final String name;
  final int age;
  
  Person(this.name, this.age);
}

 
 
 

late

late는 변수를 나중에 초기화할 것임을 명시하는 키워드입니다.

void main() {
  late String name;
  
  // 나중에 초기화
  name = 'John';
  print(name);  // OK
}

 
 

특징

null safety와 함께 사용됩니다.

class Person {
  late String name;  // null이 아닌 값으로 반드시 초기화되어야 함
  
  void init(String userName) {
    name = userName;
  }
}

 
초기화하기전에 에러가 발생합니다.

late String name;
print(name);  // Error: LateInitializationError

 
lazy initialization 이 가능합니다.

late String expensiveOperation = _loadData();
// expensiveOperation이 실제로 사용될 때만 _loadData() 호출

 
 
 

const

const는 컴파일 타임 상수를 선언할 때 사용합니다.

void main() {
  const PI = 3.14159;
  const int maxAttempts = 3;
  const String greeting = 'Hello';
}

 
 

특징

컴파일 타임에 값이 결정되어야합니다.

const time = DateTime.now();  // Error: 컴파일 타임에 값을 알 수 없음
const random = Random().nextInt(100);  // Error

 
깊은 불변성을 가집니다.

const list = [1, 2, 3];  // 리스트의 내용도 변경 불가
const map = {'name': 'John', 'age': 25};  // 맵의 내용도 변경 불가

 
메모리 효율성이 있습니다.

const v1 = [1, 2, 3];
const v2 = [1, 2, 3];
print(identical(v1, v2));  // true: 같은 메모리 공간 공유

 
 
 
 

성능과 메모리 고려사항

const vs final

  • const는 컴파일 타임 상수로, 메모리를 더 효율적으로 사용
  • final은 런타임에 값이 결정되어 약간의 오버헤드 발생

var vs dynamic

  • var는 타입이 고정되어 있어 성능상 이점
  • dynamic은 런타임 타입 체크로 인한 오버헤드 발생

late 키워드의 영향

  • 초기화 체크로 인한 약간의 런타임 오버헤드
  • 메모리는 실제 초기화 시점에만 할당

 

결론

각 키워드는 그만의 특징과 용도가 있습니다. 상황에 맞는 적절한 키워드를 선택하는 것이 중요합니다:

  • var: 지역 변수, 타입이 명확한 경우
  • dynamic: 타입이 동적으로 변하는 경우
  • final: 한 번 초기화 후 변경되지 않는 값
  • late: 지연 초기화가 필요한 경우
  • const: 컴파일 타임 상수

코드의 가독성, 유지보수성, 그리고 성능을 고려하여 적절한 키워드를 선택하시기 바랍니다.

+ Recent posts