Flutter Equatable의 이해
Flutter의 Equatable 라이브러리에 대해 알아보자
원래는 Bloc에 대해서 자세하게 작성하고 싶었지만, Bloc에 대한 내용을 잘 정리하기엔 아직 초보 플러터 개발자의 입장에서 조금 더 내용의 구체화가 필요하다고 생각이 들었다.
그래서 Bloc 공식문서 예제에 자주 등장하는 Equtable 이라는 라이브러리가 나오는데 이 라이브러리에 대해서 어떤 역할을 하고 왜 사용하는게 좋은지
이 부분도 알고 넘어가면 좋을 것 같아서 자세하게 공부해봤다.
1. Equatable이란?
Equatable은 Dart와 Flutter에서 객체 간의 값 비교를 쉽게 구현할 수 있도록 도와주는 패키지이다.
그냥 값들을 비교하면 되는거 아니야? 라고 생각할 수 있지만 전혀 아니다.
Dart에서는 기본적으로 두 객체를 비교할 때 참조 동등성(reference equality)를 사용한다.
한마디로 두 객체가 동일한 메모리 주소를 가리킬 때만 같다고 판단한다.
그래서 객체 안의 값이 같다고 하더라도 실제로 두 객체가 다른 메모리 주소를 가리키고 있기 때문에 다르다고 판단한다.
하지만 Equatable을 사용하면 값 동등성(value equality)을 쉽게 구현할 수 있다.
즉 객체의 속성 값이 같다면 두 객체를 같다고 판단할 수 있게 된다.
1.1 왜 Equatable이 필요할까?
일반적인 Dart 클래스에서는 동일한 값을 가진 두 객체를 비교할 때 문제가 발생한다.
아래의 예제를 보면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
void main() {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 27);
print(person1.name == person2.name); // true
print(person1.age == person2.age); // true
print(person1 == person2); // false
}
위 코드에서 person1
과 person2
는 같은 값을 가지고 있지만, ==
연산자는 false
를 반환한다.
아니 왜 person1 과 person2 가 다른거지?
이는 위에서 설명했듯이 Dart에서는 값이 같더라도 실제로 두 객체가 서로 다른 메모리 주소를 참조하고 있기 때문이다.
만약 Equtable을 사용하지 않고 비교를 하려면 조금 복잡해질 수 있다.
두 값에 대한 hashCode를 비교하는 방법이 있으로 구현해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
final String name;
final int age;
const Person(this.name, this.age);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Person &&
runtimeType == other.runtimeType &&
name == other.name &&
age == other.age;
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
void main() {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 27);
print(person1 == person2); // true
}
하지만 Equatable을 사용하면 이러한 문제를 해결할 수 있다.
2. Equatable 사용하기
2.1 기본 사용법
Equatable을 사용하려면 먼저 pubspec.yaml
에 의존성을 추가해야 한다
1
2
dependencies:
equatable: ^2.0.7
그런 다음 클래스에 Equatable을 적용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:equatable/equatable.dart';
class Person extends Equatable {
final String name;
final int age;
const Person(this.name, this.age);
@override
List<Object> get props => [name, age];
}
void main() {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 27);
print(person1 == person2); // true
}
Equatable을 사용하면 위와 같이 간단하게 객체 비교를 할 수 있게 해준다.
여기서 주의 해야할 점은 Equatable은 변경 불가능한 객체에서만 작동하도록 설계 되어있으므로 모든 변수가 final로 선언되어야 한다.
2.2 props 메서드 이해하기
props
getter는 Equatable의 핵심이다.
이 메서드는 객체의 동등성 비교에 사용될 속성들의 리스트를 반환한다.
props
에 포함된 속성들만 비교에 사용되므로, 필요한 속성만 포함시키는 것이 중요하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person extends Equatable {
final String name;
final int age;
final List<String> hobbies;
final DateTime birthDate;
const Person({
required this.name,
required this.age,
required this.hobbies,
required this.birthDate,
});
@override
List<Object> get props => [name, age, hobbies, birthDate];
// JSON 형식의 작업을 해야 할 경우
factory Person.fromJson(Map<String, dynamic> json) {
return Person(
name: json['name'],
age: json['age'],
hobbies: json['hobbies'],
birthDate: json['birthDate'],
);
}
}
3. Equatable의 추가적인 기능들
3.1 hashCode 자동 생성
Equatable은 props
에 정의된 속성들을 기반으로 자동으로 hashCode
를 생성한다.
이는 객체를 Set이나 Map의 키로 사용할 때 유용하다
1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 27);
print(person1.hashCode == person2.hashCode); // true
final set = {person1, person2};
print(set.length); // 1 (중복이 제거됨)
final map = {person1: 'hsungjin', person2: 'hsungjin'};
print(map.length); // 1 (중복이 제거됨)
}
3.2 toString()
Equatable은 디버깅을 위한 유용한 toString()
메서드를 제공한다.
이를 통해 객체의 상태를 쉽게 확인할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person extends Equatable {
final String name;
final int age;
const Person(this.name, this.age);
@override
List<Object> get props => [name, age];
@override
String toString() => 'Person(name: $name, age: $age)';
}
void main() {
final person = Person('hsungjin', 27);
print(person); // Person(name: hsunjin, age: 27)
}
3.3 불변성(Immutability) 보장
Equatable을 사용할 때는 클래스를 불변(immutable)으로 만드는 것이 권장된다.
불변성을 통해 객체의 상태가 변경되지 않음을 보장할 수 있다.
1
2
3
4
5
6
7
8
9
10
class ImmutablePerson extends Equatable {
final String name;
final int age;
// const 생성자 사용
const ImmutablePerson(this.name, this.age);
@override
List<Object> get props => [name, age];
}
4. 사용 예제
4.1 Bloc 패턴에서의 활용
Bloc 패턴이나 다른 상태 관리 솔루션에서 Equatable은 매우 유용하다.
상태 객체의 동등성을 쉽게 비교할 수 있어 상태 변화 감지에 효과적이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class AuthState extends Equatable {
const AuthState();
}
class AuthInitial extends AuthState {
@override
List<Object> get props => [];
}
class AuthLoading extends AuthState {
@override
List<Object> get props => [];
}
class AuthAuthenticated extends AuthState {
final String username;
const AuthAuthenticated(this.username);
@override
List<Object> get props => [username];
}
4.2 테스트에서의 활용
Equatable은 테스트 작성을 더 쉽게 만들어 준다.
객체의 동등성을 쉽게 비교할 수 있어 테스트 코드가 간결해진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
test('두 Person 객체가 같은 값을 가질 때 동등해야 함', () {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 27);
expect(person1, equals(person2));
});
test('두 Person 객체가 다른 값을 가질 때 동등하지 않아야 함', () {
final person1 = Person('hsungjin', 27);
final person2 = Person('hsungjin', 28);
expect(person1, isNot(equals(person2)));
});
}
5. 고려사항
Equatable은 매우 효율적이지만, 몇 가지 고려해야 할 사항이 있다.
필요한 속성만 참조하기
props
에는 동등성 비교에 실제로 필요한 속성만 포함해야 한다.
불필요한 속성을 포함하면 성능에 영향을 줄 수 있다
1
2
3
4
5
6
7
8
9
10
class User extends Equatable {
final String id;
final String name;
final DateTime lastUpdated; // 비교에서 제외할 수 있음
const User(this.id, this.name, this.lastUpdated);
@override
List<Object> get props => [id, name]; // lastUpdated는 제외
}
구체적인 비교
Equatable은 표면적인 비교만 수행하므로, 중첩된 객체의 경우 추가적인 처리가 필요할 수 있다. 중첩된 리스트나 맵을 비교할 때는 그에 맞는 처리를 해줘야 한다.
1
2
3
4
5
6
7
8
9
class DeepPerson extends Equatable {
final String name;
final List<String> hobbies;
const DeepPerson(this.name, this.hobbies);
@override
List<Object> get props => [name, List<String>.from(hobbies)];
}
불필요한 사용 피하기
모든 클래스에 Equatable을 적용하는 것은 좋지 않습니다.
다음과 같은 경우에만 Equatable 사용을 고려하면 좋다.
- 객체의 값 비교가 자주 필요한 경우
- Bloc의 상태(State) 클래스
- 테스트에서 객체 비교가 필요한 경우
- Collection(Set, Map 등)의 키로 사용되는 객체
성능 영향
Equatable은 편리하지만 약간의 성능 오버헤드가 있다고 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ExpensivePerson extends Equatable {
final String name;
final List<String> hobbies;
const ExpensivePerson(this.name, this.hobbies);
@override
List<Object> get props => [name, hobbies]; // 매번 새로운 리스트 생성
}
// 개선된 사용 방법
class BetterPerson extends Equatable {
final String name;
final List<String> hobbies;
const BetterPerson(this.name, this.hobbies);
// 캐시된 props 사용
static final List<Object> _props = [];
@override
List<Object> get props {
_props
..clear()
..add(name)
..add(hobbies);
return _props;
}
}
7.3 null 안전성 고려
Equatable을 사용할 때 null 안전성도 고려해야 합니다:
1
2
3
4
5
6
7
8
9
class NullSafePerson extends Equatable {
const NullSafePerson({this.name, [this.age]});
final String? name;
final int? age;
@override
List<Object?> get props => [name, age]; // Object? 사용
}
6. Equatable vs 수동 구현
때로는 Equatable 대신 직접 ==
연산자와 hashCode
를 구현하는 것이 더 적절할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ManualPerson {
final String name;
final int age;
const ManualPerson(this.name, this.age);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ManualPerson &&
other.name == name &&
other.age == age;
}
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
다음과 같은 경우에는 사용을 고려해 봐도 좋다고 한다
매우 간단한 클래스의 경우
특별한 비교 로직이 필요한 경우
성능이 매우 중요한 경우
결론
Equatable은 Flutter 애플리케이션에서 객체 비교를 단순화하고 예측 가능하게 만드는 라이브러리이다.
특히 상태 관리나 테스트 작성 시 매우 유용하며, 적절히 사용하면 코드의 품질과 유지보수성을 크게 향상시킬 수 있다.
하지만 모든 클래스에 무분별하게 적용하기보다는 실제로 값 비교가 필요한 경우에만 선택적으로 사용하는 것이 좋다.
또한 불변성과 성능을 고려하여 적절히 설계하는 것이 중요하다.