StatefulWidget의 생명주기 메서드

 

createState()

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();  // 가장 먼저 호출
}

 

initState()

class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription _subscription;
  
  @override
  void initState() {
    super.initState();  // 반드시 super 호출
    
    // 초기화 작업 수행
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen((data) => print('Stream data: $data'));
  }
}

 

didChangeDependencies()

class _MyWidgetState extends State<MyWidget> {
  ThemeData? _theme;
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    
    // 상위 위젯의 데이터 의존성 변경 시 호출
    _theme = Theme.of(context);  // InheritedWidget 데이터 접근
  }
}

 

 

build()

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('Widget Content'),
    );
  }
}

 

 

didUpdateWidget()

class _MyWidgetState extends State<MyWidget> {
  @override
  void didUpdateWidget(MyWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    // 위젯이 업데이트될 때 호출
    if (widget.someProperty != oldWidget.someProperty) {
      // 속성 변경에 따른 처리
    }
  }
}

 

 

dispose()

class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription _subscription;
  
  @override
  void dispose() {
    // 리소스 해제
    _subscription.cancel();
    super.dispose();  // 반드시 마지막에 super 호출
  }
}

 

 

생명주기 단계별 주의사항

 

initState

  • super.initState() 반드시 먼저 호출
  • context 사용 불가 (didChangeDependencies에서 사용)
  • 비동기 작업 시작 가능
  • 한 번만 호출됨

didChangeDependencies

  • InheritedWidget 데이터 접근
  • 여러 번 호출될 수 있음
  • 상위 위젯의 데이터 변경 시 호출

build

  • 순수 함수여야 함
  • 상태 변경 금지 (setState 호출 금지)
  • 가능한 가벼운 연산만 수행

dispose

  • 리소스 해제 필수
  • super.dispose() 마지막에 호출
  • mounted 체크 후 setState 호출

 

일반적인 실수와 해결방법

 

메모리 누수 방지

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription? _subscription;
  
  @override
  void initState() {
    super.initState();
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen((data) {
          if (!mounted) return;  // mounted 체크
          setState(() {
            // 상태 업데이트
          });
        });
  }
  
  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
}

 

비동기 작업 처리

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    _loadData();
  }
  
  Future<void> _loadData() async {
    try {
      await Future.delayed(Duration(seconds: 2));
      if (!mounted) return;
      setState(() {
        // 상태 업데이트
      });
    } catch (e) {
      print('Error: $e');
    }
  }
}

 

 

Context 사용

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // Error: context 사용 불가
    // Theme.of(context);
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // OK: context 사용 가능
    final theme = Theme.of(context);
  }
}

 

 

마치며

위젯의 생명주기를 잘 이해하고 적절히 활용하면 메모리 누수를 방지하고 효율적인 상태 관리가 가능합니다. 특히 dispose() 메서드에서의 리소스 해제와 비동기 작업 시의 mounted 체크는 매우 중요합니다.

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

[Flutter] 앱 아이콘 변경  (0) 2024.11.22
[Flutter] 앱 이름 변경  (0) 2024.11.21
[Dart] fold() 메소드  (0) 2024.11.19
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Spread 연산자(...)  (0) 2024.11.17

개요

오늘은 List의 fold() 메소드에 대해서 빠르게 알아봅시다.

 

  T fold<T>(T initialValue, T combine(T previousValue, E element)) {
    var value = initialValue;
    for (E element in this) value = combine(value, element);
    return value;
  }

 

생신건 이렇게 생겼습니다. JAVA만 파던 저에게는 뭐하는 메소드인지 감이 잘 안잡혔습니다. 함께 알아보도록 하죠.

 

 

fold()

fold는 initialValue을 시작으로 combine 값을 연산하는 방식입니다.

 

간단하게 수익률을 계산하는 메소드를 만들어보겠습니다.

void main() {

  double money = 1000000;

  List<double> rate = [5, -3.4, 0, 1.7, -4.5];

  double result = rate.fold(money,
      (previousValue, element) => previousValue + (previousValue * (element / 100)),);

}

 

fold 첫번째 인자에는 최초 시작값을 정해줍니다. 초기자금인 1,000,000원을 최초 시작값으로 설정합니다.

두번째 인자에는 T combine(T previousValue, E element) 를 넣어줍니다.

previousValue는 이전 계산된 값이고, element는 List에서 현재 인덱스가 가리키고있는 값을 의미합니다.

즉 fold가 첫번째 iterate 를 예를 들면 1000000 + (1000000 * (5 / 100)) 가 되는 것입니다.

 

 

오늘은 간단하게 fold 메소드에 대해서 알아보았습니다.

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

[Flutter] 앱 이름 변경  (0) 2024.11.21
[Flutter] 위젯 생명주기(Lifecycle)  (0) 2024.11.20
[Flutter] 상태 관리 기초 - setState  (0) 2024.11.18
[Dart] Spread 연산자(...)  (0) 2024.11.17
[Dart] Null Safety  (0) 2024.11.16

개요

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: 컴파일 타임 상수

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

StringTokenizer

StringTokenizer는 문자열을 지정된 구분자를 기준으로 토큰화하는 Java의 유틸리티 클래스입니다.

 

        String str = "a;b;c;d;e";

        StringTokenizer st = new StringTokenizer(str, ";");
        while (st.hasMoreTokens()) {
            System.out.print(st.nextToken()); // abcde
        }
        
        StringTokenizer st = new StringTokenizer(str, "abc");
        while (st.hasMoreTokens()) {
            System.out.print(st.nextToken()); // de
        }

가령 "a;b;c;d;e" 라는 문자열에서 ";" 문자열로 나누어 토큰화 하고싶다면 위 처럼 사용하는 것입니다.

위와 같은 역할을 수행하는게 split() 메소드인데요 split() 메소는 정규식을 사용하기때문에 활용성이 높고 정규식을 잘 이해하고있다면 더욱 편한게 사실입니다.

그러면 이 StringTokenizer를 왜 사용했을까요? split() 메소드는 내부에서 정규식을 사용하기때문에 StringTokenizer보다 속도가 4배정도 느리다고 합니다. 그래서 속도가 중요한 부분에서 이 StringTokenizer를 사용했었죠. 하지만 현재는 사용하지않는것을 권장하고있습니다. 이 글에서 이 부분에 대해서 알아보겠습니다.

 

 

 

 

Document

JAVA 5 StringTokenizer

JAVA 6 StringTokenizer

JAVA 7 StringTokenizer

JAVA 8 StringTokenizer

JAVA 9 StringTokenizer

JAVA 10 StringTokenizer

JAVA 11 StringTokenizer

JAVA 21 StringTokenizer

 

StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead.

StringTokenizer는 호환성을 이유로 유지되는 레거시 클래스이지만 새 코드에서는 사용이 권장되지 않습니다. 이 기능을 원하는 사람은 대신 String의 split 메서드나 java.util.regex 패키지를 사용하는 것이 좋습니다.

 

문서에는 StringTokenizer는 새 코드에서는 사용하는것을 권장하지 않는다고 작성되어있습니다.

 

StringTokenizer의 주석에도 권장하지 않는다고 하는데 왜 그런지 간단하게 알아보겠습니다.

 

 

 

 

권장하지 않는 이유

 

유연성의 문제

StringTokenizer는 매우 단순한 방식으로 문자열을 분리합니다. 기본적으로 정규 표현식을 지원하지 않기 때문에 복잡한 문자열 처리에는 한계가 있습니다.  split() 메소드나 java.util.regex 패키지를 사용하면 정규표현식을 지원하여 복잡한 구분자를 처리하여 더 유연하고 안정적인 방식으로 문자열을 분리할 수 있습니다. 그러니 StringTokenizer를 사용할 이유가 없어지고 있는 것이지요.

 

 

클래스 개선문제

StringTokenizer는 Java 초기에 도입된 클래스입니다. 이후 자바가 발전하면서 더 나은 기능을 제공하는 클래스들이 도입되면서 StringTokenizer의 사용이유가 더욱 줄어들었습니다. 따라서 새로운 기능이 추가되거나 개선되지 않습니다.

 

제한된 기능

정규표현식을 지원하지않고, 복합한 문자열 처리에 한계가있습니다. 또한 반환 타입이 String[] 같은 배열이 아닙니다.

 

 

 

 

결론

StringTokenizer의 장점도 존재합니다. 정규표현식이 없기때문에 상대적으로 처리속도가 빠릅니다. (split의 약 4배)

그리고 간단한 문자열을 처리하기에는 StringTokenizer를 사용하는것이 더 적합할 수 있습니다.

 

결론은 더 이상 StringTokenizer를 사용하지 말고 split 메소드를 사용하는게 현대적인 방법이라고 생각합니다.

 

개요

오늘은 Java 14부터 도입된 Record와 Java 15에서 preview로 시작된 Sealed Classes 에 대해서 알아보겠습니다.
참고로 Sealed Classes 는 Java 17부터 정식적으로 확정된 것같습니다.

 

JDK 17 Release Notes, Important Changes, and Information

These notes describe important changes, enhancements, removed APIs and features, deprecated APIs and features, and other information about JDK 17 and Java SE 17. In some cases, the descriptions provide links to additional detailed information about an issu

www.oracle.com

 
 

Record

Record는 불변(immutable) 데이터 객체를 생성하기 위한 새로운 유형의 클래스입니다. 주로 데이터를 운반하는 목적으로 사용되며, 기존의 많은 코드를 줄여줍니다.
 

Record 적용 전

public class Student {

    private final String name;
    private final int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Student[name=" + name + ", age=" + age + "]";
    }

}

 

Record 적용 후

public record Student(String name, int age) { }

 

  • 모든 필드가 private final로 선언됩니다
  • 자동으로 equals(), hashCode(), toString() 메서드가 생성됩니다
  • 생성자와 접근자(getter)가 자동으로 생성됩니다
  • 상속이 불가능합니다

IntelliJ에서 record 를 추천하기도합니다.

 

    public static void main(String[] args) {
        Student student = new Student("철수", 20);

        System.out.println("student = " + student);
        System.out.println("student.name() = " + student.name());
        System.out.println("student.age() = " + student.age());
        
        // student = Student[name=철수, age=20]
        // student.name() = 철수
        // student.age() = 20
    }

 
 
 
 

Sealed Classes

Sealed Classes는 상속을 제한하는 새로운 방법을 제공합니다. 클래스나 인터페이스가 어떤 클래스에 의해 상속(extends)/구현(implements) 될 수 있는지 명시적으로 선언할 수 있습니다.
 

public sealed class Shape permits Circle, Rectangle, Square {
    // 공통 속성 및 메서드
}

public final class Circle extends Shape {
    private final double radius;
    // Circle 구현
}

public final class Rectangle extends Shape {
    private final double width;
    private final double height;
    // Rectangle 구현
}

public final class Square extends Shape {
    private final double side;
    // Square 구현
}

 
permits 한 클래스에서 sealed class를 구현하지 않거나, permits 에 명시되지않은 클래스에서 sealed 클래스를 구현하려고하면 에러가 발생합니다.
 

public sealed class Shape permits Circle, Rectangle, Square { // 에러!!
    // 공통 속성 및 메서드
}

public class Circle {
    private final double radius;
    // Circle 구현
}
public sealed class Shape permits Circle, Rectangle, Square {
    // 공통 속성 및 메서드
}

public final class Triangle extends Shape { // 에러!!

}

 

  • 상속/구현하는 클래스는 final, non-sealed, sealed 중 하나를 선언해야 합니다.
  • sealed class와 permit 된 subclass는 한 패키지 내에 존재해야 합니다.
  •  
키워드 설명
final 더 이상의 상속을 금지
sealed 제한된 상속
non-sealed 상속 허용

 

final

public final class Circle extends Shape {
    // Circle 구현
}

 

sealed

public sealed class Rectangle extends Shape permits ColoredRectangle {
    // Rectangle 구현
}

 

non-sealed

public non-sealed class Square extends Shape {
    // Square 구현
}

 
 

Sealed Classes 를 써야하는 이유가 있을까?

먼저 Sealed Classes를 도입하면 좋은 점을 알아보겠습니다.
 

타입 안정성 강화

  • 상속 가능한 클래스를 명확히 제한해서 타입 안정성을 높힙니다.
  • 컴파일 시점에서 가능한 모든 하위 타입을 알 수 있습니다.
  • switch 문법에서 하위 클래스가 모두 존재한다는 것을 컴파일러가 보장합니다. (switch JAVA 21 문법)

도메인 모델링 개선

  • 도메인 모델의 제약조건을 코드로 표현할 수 있습니다
  • 특정 타입이 가질 수 있는 모든 변형을 명시적으로 정의할 수 있습니다

 
sealed로 온라인 결제 시스템에서 지원하는 결제 수단을 예시로 만들어보겠습니다.

public sealed interface PaymentMethod permits 
    CreditCard, 
    BankTransfer, 
    DigitalWallet {
    
    boolean process(int amount);
    String getPaymentInfo();
}

public final class CreditCard implements PaymentMethod {
    private final String cardNumber;
    private final String expiryDate;
    private final String cvv;

    @Override
    public boolean process(int amount) {
        // 신용카드 결제 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Card: " + cardNumber.substring(12);
    }
}

public final class BankTransfer implements PaymentMethod {
    private final String accountNumber;
    private final String bankCode;

    @Override
    public boolean process(int amount) {
        // 계좌이체 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Bank: " + bankCode + "-" + accountNumber;
    }
}

public final class DigitalWallet implements PaymentMethod {
    private final String walletId;
    
    @Override
    public boolean process(int amount) {
        // 디지털 월렛 결제 처리 로직
        return true;
    }

    @Override
    public String getPaymentInfo() {
        return "Wallet: " + walletId;
    }
}

// 결제 처리 서비스
public class PaymentService {
    public String processPayment(PaymentMethod method, int amount) {
    	// JDK 21 switch 문법
        return switch (method) {
            case CreditCard card -> 
                "신용카드 결제: " + card.getPaymentInfo();
            case BankTransfer transfer -> 
                "계좌이체: " + transfer.getPaymentInfo();
            case DigitalWallet wallet -> 
                "디지털 월렛: " + wallet.getPaymentInfo();
        }; // 모든 결제 수단이 처리됨을 컴파일러가 보장
    }
}

 

  • 지원하는 결제 수단을 명확하게 제한할 수 있습니다.
  • 새로운 결제 수단 추가 시 컴파일러가 관련된 모든 코드를 체크합니다.
  • switch 문에서 모든 케이스를 처리했는지 컴파일 시점에 확인할 수 있습니다.

 

개요

오늘은 객체 지향 프로그래밍(OOP)의 핵심 개념인 상속(Inheritance)과 복합(Composition)에 대해 설명해드리겠습니다. 이 두 개념은 코드 재사용과 유지보수성을 높이는 중요한 설계 방식입니다. 예제와 함께 특징을 살펴보며 공부해도록 하겠습니다.

 

 

 

상속

상속은 OOP에서 가장 기본개념중에 하나입니다. 부모 - 자식 클래스로 정의하여 자식클래스에서 부모 클래스의 메소드를 override, 재사용 할 수 있다는 특징이 있습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;

    public void start() {
        System.out.println("차량이 시동됩니다.");
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

 

public class ElectricCar extends Car {

    private int batteryCapacity;
    private int batteryLevel;

    public ElectricCar(String brand, String model, int year, int batteryCapacity) {
        super(brand, model, year);
        this.batteryCapacity = batteryCapacity;
        this.batteryLevel = 100;
    }

    @Override
    public void start() {
        System.out.println("전기차가 조용히 시동됩니다.");
    }

    public void charge() {
        System.out.println("배터리를 충전합니다.");
        this.batteryLevel = 100;
    }
    
}

 

ElectricCar 클래스는 Car 클래스를 상속받고있습니다. 따라서 Car와 ElectricCar는 부모-자식 관계입니다.

ElectricCar는 Car의 기능을 재사용하거나 새로운 기능을 손쉽게 추가할 수 있고, 때에 따라서 Override 할 수 있습니다. 그리고 Car 에서 변경된 사항은 ElectricCar에도 그대로 반영됩니다.

 

 

 

복합

@AllArgsConstructor
public class Engine {

    private final String type;
    private final int hp;

    public void start() {
        System.out.println(type + " 엔진이 가동됩니다.");
    }

}

 

@AllArgsConstructor
public class GasCar {

    private final Car car; // 복합관계
    private final Engine engine; // 복합관계

    public void start() {
        car.start();
        engine.start();
    }
}

 

Engine 클래스와 GasCar 클래스를 구현했습니다. GasCar는 Car와 Engine을 필드에서 참조하고있습니다.

이처럼 GasCar가 Car를 필드로 가지는 것이 복합관계라고합니다. GasCar를 통해 Car 메소드를 호출하는 방식이죠.

 

 

상속의 장단점

장점

  1. 코드 재사용성
    • 부모 클래스의 기능을 그대로 물려받아 사용할 수 있어 코드 중복을 줄일 수 있습니다.
    • 공통 기능을 부모 클래스에 구현하면 모든 자식 클래스가 활용할 수 있습니다.
  2. 간단한 확장성
    • 기존 클래스를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
  3. 다형성 구현
    • 부모 타입으로 자식 객체를 참조할 수 있어 유연한 프로그래밍이 가능합니다.
Car car = new ElectricCar("Tesla", "Model 3", 2024, 75);

 

단점

  1. 강한 결합도
    • 부모 클래스의 변경이 모든 자식 클래스에 영향을 미칩니다.
    • 부모 클래스의 내부 구현을 자식 클래스가 알아야 할 수 있습니다.
  2. 취약한 기반 클래스 문제
    • 부모 클래스의 변경이 예상치 못한 자식 클래스의 동작 변경을 일으킬 수 있습니다.
  3. 단일 상속 제한
    • Java는 다중 상속을 지원하지 않아 한 클래스만 상속할 수 있습니다.

 

복합의 장단점

장점

 

  1. 유연한 설계
    • 런타임에 구성요소를 변경할 수 있습니다.
    • 다중 복합이 가능합니다.
  2. 캡슐화 향상
    • 내부 구성요소의 구현을 완전히 숨길 수 있습니다.
    • 인터페이스를 통한 통신이 가능합니다.
  3. 느슨한 결합도
    • 각 클래스가 독립적으로 동작하여 유지보수가 용이합니다.
// Engine을 쉽게 교체할 수 있음
public void updateEngine(Engine newEngine) {
    this.engine = newEngine;
}

 

 

 

단점

  1. 코드 가독성
    • 복잡한 구성의 경우 코드 추적이 어려울 수 있습니다.
  2. 구현의 복잡성
    • 여러 클래스를 조합하여 사용하므로 초기 설계가 더 복잡할 수 있습니다.
// 복합 관계에서는 위임 메서드를 많이 작성해야 할 수 있음
public class GasCar {
    private Car car;
    private Engine engine;
    
    public void start() { car.start(); }
    public void stop() { car.stop(); }
    public void accelerate() { engine.accelerate(); }
    // ... 더 많은 위임 메서드들
}

 

 

결론

상속과 복합에 대한 글을 읽다보면 'IS-A' 일때는 상속, 'HAS-A' 일때는 복합을 사용하라는 글이 많습니다.

'전기차는 차다' -> 상속,

'차는 엔진을 갖고있다' -> 복합,

이렇게 이해하면 조금더 와닿을것 같습니다.

 

최근의 객체지향 설계에서는 "상속보다는 복합을 사용하라"는 원칙이 널리 받아들여지고 있습니다. 복합이 더 유연하고 안전한 설계를 제공하기 때문입니다. 하지만 각각의 상황에 맞는 적절한 선택이 중요합니다.

 

마지막으로 위의 예제를 상속과 복합관계를 이용하여 CasCar를 조금 더 깔끔하게 바꿔보면서 글을 마치겠습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;
    private final Engine engine;

    public void start() {
        System.out.println("차량이 시동됩니다.");
        engine.start();
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

public class GasCar extends Car {

    private final int fuelCapacity;
    private double currentFuel;

    public GasCar(String brand, String model, int year, Engine engine, int fuelCapacity) {
        super(brand, model, year, engine);
        this.fuelCapacity = fuelCapacity;
        this.currentFuel = fuelCapacity;
    }

    public void refuel(double amount) {
        currentFuel = Math.min(currentFuel + amount, fuelCapacity);
    }

}

 

차는 엔진을 가지고있다 -> private final Engine engine

가솔린차는 차이다 -> GasCar extends Vehicle

 

개요

Flutter에는 부모위젯으로부터 제약을 받아 자신의 크기를 결정하는 위젯들이 존재합니다.

만약 부모의 크기가 정해지지 않다면 자신의 크기를 정하지 못해 오류가 발생합니다. 이에 대해서 알아봅시다.

 

 

Column - Column - (Expanded, Flexible)

Expanded와 Flexible은 부모의 사이즈를 채워주는 위젯입니다.

Expanded나 Flexible를 적용한 모습

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded( // Flexible 로 마찬가지
        child: Container(
          color: Colors.blue,
          child: ...,
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

Column 안에 Expanded를 넣으면 깔끔하게 채워줍니다. 이게 가능한 이유는

  1. Column / Row 는 부모로부터 제약조건을 받아야한다. 없다면 하위 위젯에 크기에 따라서 정해진다.
  2. Scaffold 의 body사이즈는 MediaQuery의 크기(화면 크기)에서 AppBar, BottomNavigationBar등이 차지하는 공간을 제외한 나머지 영역의 크기로 정해져있어 크기 제약조건이 존재한다.
  3. Column의 부모위젯에 제약조건이 존재한다.

따라서 Expanded 위젯을 사용하더라도 오류가 발생하지 않습니다.

 

 

그런데 만약에 Column - Column 안에 Expanded가 들어간다면

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Column(
        children: [
          Expanded(
            child: Container(
              color: Colors.blue,
              child: ...,
            ),
          ),
        ],
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

Column 안에 Column 에 Expanded를 하면 

RenderFlex children have non-zero flex but incoming height constraints are unbounded

오류가 발생합니다.

 

그 이유는

  1. 내부 Column의 크기 제약조건이 존재하지않는다.
  2. 내부 Column의 크기는 그 하위 위젯의 크기에 따라 결정된다.
  3. 내부 Column안에 Expanded 위젯은 부모의 크기제약에 따라 크기가 결정된다.

2번과 3번이 서로 크기 제약조건을 의존하고있는걸 확인할 수 있습니다. 따라서 오류가 발생하는것입니다.

 

이 같은 조건은 Row - Row에서도 동일하게 동작합니다.

 

 

ListView

ListView는 기본적으로 크기제약이 없고 가능한 모든 공간을 사용하려고 합니다. 제 기준 가장 사용하기 까다로운 위젯입니다. 왜냐하면 ListView를 단독으로 사용할일이 거의 없기 때문입니다.

 

보통 SingleChildScrollView 위젯내에서 사용하거나 Column 위젯에서 ListView를 사용하는데 오류가 가장 많이 나더라구요.

 

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) => Text('텍스트$index'),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이렇게 Column 안에 놓는다? Vertical viewport was given unbounded height. 바로 오류납니다.

 

위와 마찬가지로 ListView의 크기제약이 없어서 그렇습니다. 이 같은 상황에 해결법은 Expanded 위젯을 사용하는겁니다.

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded(
        child: ListView.builder(
          itemCount: 10,
          itemBuilder: (context, index) => Text('텍스트$index'),
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이제 오류가 안납니다. 그런데 보통 body에는 처음에 SingleChildScrollView 를 사용해서 전체 스크롤을 가능하게 해줍니다. 화면크기만으로는 컨텐츠를 몇개 못담으니까요.

 

 

body: SingleChildScrollView(
    child: Column(
      children: [
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
        Expanded(
          child: ListView.builder(
            itemCount: 10,
            itemBuilder: (context, index) => Text('텍스트$index'),
          ),
        ),
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
      ],
    ),
  )

 

RenderFlex children have non-zero flex but incoming height constraints are unbounded.

 

 

  1. SingleChildScrollView는 자식(Column)에게 높이 제약을 전달
  2. Column은 이 unbounded 제약을 받은 상태에서 Expanded를 만남, 즉 자식위젯들에게 높이 제약을 전달
  3. Expanded는 남은 공간을 차지하려고 하는데, unbounded 상태에서는 '남은 공간'이 얼마인지 계산할 수 없음
  4. 따라서 오류 발생

네 이럴때는 Expanded를 제거해야합니다. 하지만 여기에서 Expanded를 제거한다고 해결되지 않습니다.

 

Vertical viewport was given unbounded height.

오류가 발생합니다.

 

이도 ListView 첫번째 예제와 똑같이 생겼거든요. 그럼 어떻게 해결해야할까요?

 

shrinkWrap: true,

 

ListView에 속성중에 shrinkWrap 옵션이 있습니다. 해당 옵션은 false가 default인데, 이 옵션이 true가 되면 ListView가 자신의 자식들의 높이 합만큼만 크기를 가지도록 만듭니다. 그 말은 무한한 크기를 가지려는 성질을 제한하는 옵션이라는 겁니다. 이 옵션을 사용하면 SingleChildScrollView - Column - ListView 관계를 해결할 수 있습니다.

 

다만, 단점으로는 모든 자식의 높이를 계산해서 자신의 크기를 정해야 하기때문에 자식이 많을수록 성능저하가 발생할 수 있다는 겁니다. 따라서 사용할때는 주의를 갖고 사용해야합니다.

 

 

+ Recent posts