Freezed 개요
Freezed는 Flutter에서 **불변 데이터 클래스(Immutable Data Class)**를 자동으로 생성하는 패키지로, 코드 작성의 간편함과 유지보수성을 크게 향상시킵니다.
주요 기능과 장점:
- 불변성 보장: 데이터가 변경되지 않도록 하여 안전한 상태 관리를 돕습니다.
copyWith
자동 생성: 일부 필드만 변경된 객체를 쉽게 생성할 수 있습니다.- 객체 비교 및 해시 코드 자동 생성:
==
연산자와hashCode
를 자동 생성해 객체 비교가 간편합니다. - JSON 직렬화 지원:
json_serializable
과 연계해 JSON 데이터를 쉽게 변환합니다. - 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_annotation
과 json_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
필드가 리스트임에도 불구하고 내부 요소를 정확하게 비교하여 user1
과 user2
를 동일한 객체로 판단합니다. 리스트나 맵이 포함된 객체도 안전하게 비교할 수 있어, 데이터 정확성을 보장합니다.