flutter – freezed


Freezed 개요

Freezed는 Flutter에서 **불변 데이터 클래스(Immutable Data Class)**를 자동으로 생성하는 패키지로, 코드 작성의 간편함과 유지보수성을 크게 향상시킵니다.

주요 기능과 장점:

  1. 불변성 보장: 데이터가 변경되지 않도록 하여 안전한 상태 관리를 돕습니다.
  2. copyWith 자동 생성: 일부 필드만 변경된 객체를 쉽게 생성할 수 있습니다.
  3. 객체 비교 및 해시 코드 자동 생성: == 연산자와 hashCode를 자동 생성해 객체 비교가 간편합니다.
  4. JSON 직렬화 지원: json_serializable과 연계해 JSON 데이터를 쉽게 변환합니다.
  5. Union 타입 지원: 다양한 상태(로딩, 성공, 실패)를 쉽게 관리할 수 있어, 상태 관리가 단순해집니다.

설치 및 설정

1. 패키지 설치

pubspec.yaml 파일에 다음 의존성을 추가합니다:

dependencies:
  freezed_annotation: ^2.0.0
  json_annotation: ^6.0.0  # JSON 직렬화를 사용할 경우 (선택 사항)

dev_dependencies:
  build_runner: ^2.0.0  # 코드 생성기
  freezed: ^2.0.0
  json_serializable: ^6.0.0  # JSON 직렬화 메서드를 자동 생성할 경우 (선택 사항)

freezed_annotation은 Freezed 모델에 어노테이션을 추가하는 데 사용되며, freezed는 코드 생성기를 통해 자동으로 메서드와 클래스를 생성하는 데 필요합니다.json_annotationjson_serializabl 은 는 JSON 직렬화 및 역직렬화 기능의 자동 생성 해주는데 도움을 줍니다.

2. Freezed 모델 생성 및 코드 생성기 설정

Freezed는 코드 생성 방식을 사용하므로, 모델 파일을 정의한 후 코드 생성기를 실행하여 Freezed가 필요한 메서드와 클래스를 자동으로 생성하게 해야 합니다. 이를 위해 다음 단계를 수행합니다:

모델 파일 정의: Freezed 모델을 정의할 파일에 part 키워드를 추가해 필요한 파일이 자동 생성되도록 준비합니다.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart'; // JSON 직렬화를 사용할 경우 필요

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required int age,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

코드 생성 실행: 모델 파일을 정의한 후, 터미널에서 아래 명령어를 실행하여 Freezed의 메서드 및 클래스가 포함된 파일들을 생성합니다.

flutter pub run build_runner build --delete-conflicting-outputs

기본 사용법

Immutable 데이터 클래스 생성의 이유와 장점

Freezed로 생성한 데이터 클래스는 **불변성(Immutable)**을 갖습니다. 이는 객체가 생성된 후에는 객체 내 데이터가 변경되지 않음을 의미합니다.

Immutable의 장점:

  • 안정성: 데이터가 변경되지 않으므로 예기치 않은 데이터 변경을 방지합니다.
  • 성능: 값이 바뀔 때마다 새로운 객체를 생성하므로, 변경된 상태가 필요한 경우라도 기존 객체에 영향을 주지 않습니다.
  • 예측 가능성: 객체가 항상 동일한 상태를 유지하므로 코드의 예측 가능성과 가독성이 높아집니다.

불변 데이터는 상태가 쉽게 예측 가능하므로 상태 관리에서 큰 장점을 가집니다. 특히, 여러 위젯이 데이터를 공유하거나, 데이터가 외부로부터 쉽게 변경되지 않도록 해야 하는 경우 매우 유용합니다.

Freezed는 몇 가지 유용한 메서드를 자동으로 생성하여, 코드 작성이 간편해지고 객체 관리가 쉬워집니다. 주요 자동 생성 메서드는 다음과 같습니다.

copyWith

  • 기능: 객체를 복사하면서 특정 속성만 변경된 새로운 객체를 만들 수 있습니다.
  • 사용 예: 상태 관리에서 객체의 일부 속성만 바꾸고 싶을 때 유용합니다.
final user = User(id: '123', name: 'Alice', age: 30);
final updatedUser = user.copyWith(name: 'Bob'); // name만 변경된 새로운 객체 생성
print(updatedUser); // 출력: User(id: 123, name: Bob, age: 30)

toString

  • 기능: 객체의 필드와 값을 포함한 문자열을 자동으로 생성하여, 객체의 상태를 쉽게 확인할 수 있습니다.
  • 사용 예: 디버깅 시 객체의 현재 상태를 확인할 때 유용합니다.
final user = User(id: '123', name: 'Alice', age: 30);
print(user); // 출력: User(id: 123, name: Alice, age: 30)

== 연산자hashCode

  • 기능: 객체의 모든 필드 값이 동일한지 비교하여, 두 객체가 같은지 확인할 수 있도록 합니다.
  • 사용 예: 데이터가 변경되지 않는지 확인하거나, 동일한 상태인지 판단하는 로직에서 유용합니다.
final user1 = User(id: '123', name: 'Alice', age: 30);
final user2 = User(id: '123', name: 'Alice', age: 30);
print(user1 == user2); // 출력: true, 모든 필드 값이 같으므로 동일 객체로 판단
print(user1.hashCode == user2.hashCode); // 출력: true

이러한 메서드들이 자동으로 생성되므로, 객체의 생성과 관리가 직관적이고 오류 발생 가능성이 낮아지며, 유지보수성이 크게 향상됩니다.

Annotation과 기능

  • @freezed: 기본 클래스 정의로, 데이터 클래스를 자동 생성합니다.
  • @JsonSerializable: JSON 직렬화 및 역직렬화 메서드를 자동 생성하여 JSON 데이터를 쉽게 변환할 수 있게 합니다.
  • @Default: 필드에 기본값을 설정하여 코드의 간결성을 높입니다. 인스턴스를 직접 생성할 때 기본값을 적용합니다.
  • @JsonKey(defaultValue): JSON에서 해당 필드가 누락되었을 때만 기본값을 사용하며, Dart에서 인스턴스를 직접 생성할 때는 기본값을 적용하지 않습니다.
  • @Assert: 특정 조건을 설정해 데이터 검증을 자동화하고, 무결성을 유지합니다.
@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    @Default(18) int age, // age의 기본값을 18로 설정
  }) = _User;
}

// JSON 직렬화를 위한 메서드
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

@Assert('age >= 0', 'Age cannot be negative')
  const User._(); // 생성자 내에서 age가 0보다 작은 경우 예외 발생
  • @JsonKey(fromJson: _fromJson, toJson: _toJson) : 필드를 JSON으로 변환할 때 와 그 반대의 경우 커스텀 변환 함수를 사용
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,

    // createdAt 필드를 커스텀 변환 함수로 변환
    @JsonKey(fromJson: _fromJson, toJson: _toJson) DateTime? createdAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);

  // JSON의 String -> DateTime 변환 함수
  static DateTime _fromJson(String date) => DateTime.parse(date);

  // DateTime -> JSON의 String 변환 함수
  static String _toJson(DateTime date) => date.toIso8601String();
}

createdAt 필드는 JSON에서 가져올 때 문자열을 DateTime으로 변환합니다. 반대로, User 객체를 JSON으로 변환할 때는 DateTime을 ISO 8601 형식의 문자열로 변환하여 저장합니다.

Freezed와 Provider 결합 예제

Freezed와 Provider를 결합하면 상태 관리와 데이터 모델링이 간편해집니다. 특히 NotifierProvider를 통해 Freezed 데이터 클래스를 상태로 관리할 때 매우 유용합니다.

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

part 'counter_state.freezed.dart';

@freezed
class CounterState with _$CounterState {
  const factory CounterState({
    required int count,
  }) = _CounterState;
}

// NotifierProvider 사용
final counterProvider = NotifierProvider<CounterNotifier, CounterState>(() => CounterNotifier());

// Notifier 클래스를 사용한 CounterNotifier
class CounterNotifier extends Notifier<CounterState> {
  @override
  CounterState build() {
    return const CounterState(count: 0);
  }

  void increment() => state = state.copyWith(count: state.count + 1);
  void decrement() => state = state.copyWith(count: state.count - 1);
}

DeepCollectionEquality 기능

DeepCollectionEquality는 리스트나 맵과 같은 컬렉션의 깊은 비교를 수행하기 위한 도구입니다. Freezed는 객체가 복잡한 컬렉션을 포함하더라도 컬렉션의 내용까지 비교할 수 있도록 DeepCollectionEquality를 자동으로 적용합니다. List나 Map과 같은 복잡한 필드도 동일성을 정확하게 비교할 수 있습니다.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required List<String> roles,
  }) = _User;
}

void main() {
  final user1 = User(id: '123', name: 'Alice', roles: ['admin', 'user']);
  final user2 = User(id: '123', name: 'Alice', roles: ['admin', 'user']);

  print(user1 == user2); // 출력: true
}

Freezed는 roles 필드가 리스트임에도 불구하고 내부 요소를 정확하게 비교하여 user1user2를 동일한 객체로 판단합니다. 리스트나 맵이 포함된 객체도 안전하게 비교할 수 있어, 데이터 정확성을 보장합니다.