View
어제부터 FutureProvider.family로 상태 관리 로직을 추가해보려고 수많은 뻘짓과 문서탐색과 노가다를 하고있었다. 그리고 오늘의 글을 작성하려는 목적이 여기에서의 파라미터 타입에서 시작되었다!!!
ㅤ
Usecase를 호출하기 위한 파라미터 데이터를 묶어줄 타입을 이렇게 클래스로 만들어주고 FutureProvider로 param을 넘겨 데이터를 호출하였다.
// 클래스 타입을 이렇게 정의해주고
class MyUsecaseParam {
MyUsecaseParam({required this.id, required this.date});
final String id;
final DateTime date;
}
// 아래처럼 호출하였음
final param = MyUsecaseParam(id: ..., date: ...);
final state = ref.watch(myFutureStateProvider(param));
ㅤ
그런데 왠걸? 매 프레임마다 FutureProvider가 계속 새로 호출되어서 state가 매 프레임마다 갱신되고 있었다. UI 상태 자체도 결과값에 대해서 분리해서 그려주도록 구현해뒀는데 Loading 상태의 UI만 보여졌다.
ㅤ
문제를 좀 찾아보다가 파라미터가 단순한 primitive 타입이 아니라 클래스 타입이라서 문제가 있을 수 있겠다 싶었다. 보통은 속성 값이 같더라도 인스턴스가 다르면 다른 값으로 인식을 하게되니깐. ( ← 이게 맞았음. 근데 해결을 못해서 뻘짓을 크게 한 바퀴 돌게되었음 😩 )
ㅤ
해결을 위해서는 1) 실제로 같은 인스턴스의 파라미터를 넘기기, 2) 다른 인스턴스라도 속성 값이 같으면 같은거라고 처리하기, 요 2개의 방법이 있다고 생각했다. 이번에 구현해보려는게 외부에서 상태를 주입하지 않고 위젯 내에서 파라미터를 정제하고 FutureProvider로 각자 자신의 데이터 상태를 직접 호출해 UI를 그리는 방식이라서, 1번처럼 외부에서 파라미터 값을 만들어 전달해주는 것 대신에 2번 방법으로 진행해보려 했다.
ㅤ
다른 언어에서는 Equatable 같은 인터페이스를 채택하고 비교 메서드를 오버로딩해서 같은 클래스의 두 인스턴스라도 속성 값이 같으면 같은 데이터로 처리하도록 했던 경험이 있어서 비슷한 방법을 시도해보려 했고, 플러터의 Equatable 패키지가 딱 이걸 위한 패키지인 것 같아서 적용을 시도했다.
https://pub.dev/packages/equatable
ㅤ
Equatable 패키지의 문서에 나와있는 것들을 참고해, 두 인스턴스를 비교했을 때 같은 값이라고 여기도록 props, stringify 의 오버라이드를 작성해줬다. 그런데도 똑같이 매 프레임마다 상태를 새로 호출하는 미친 코드를 뽑아냈다. (파이어베이스에 데이터를 요청하는 코드라, 자칫하면 오늘 사용가능한 API 횟수를 초과할 뻔 했다.)
ㅤ
내부적으로 Equatable 패키지 자체가 해시값에 대한 생성을 해주기 때문에, 별 문제는 없을 거라 생각했지만 hashCode 에 대한 override도 추가해줬다. 그럼에도 여전히 작동하지 않더라.
class MyUsecaseParam implements Equatable {
MyUsecaseParam({required this.id, required this.date});
final String id;
final DateTime date;
@override
List<Object?> get props => [id, date];
@override
bool? get stringify => false;
@override
int get hashCode => id.hashCode ^ date.hashCode;
}
ㅤ
디버거 상에서도 여기에서 전달되는 Param 값이 항상 동일했다. id 정보와 date 정보도 동일하고 hashCode도 항상 동일한 값이 전달되고 있었다. (확인해보니 DateTime의 내부 hash 도 동일했음)
ㅤ
그런데 웁스? 이게 웬걸? Equatable 패키지 대신에 직접 == 연산자에 대한 구현을 추가했더니 정상적으로 작동했다. 일단은 문제 해결. 그리고 패키지에 대한 신뢰감 하락.
class MyUsecaseParam {
MyUsecaseParam({required this.id, required this.date});
final String id;
final DateTime date;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MyUsecaseParam &&
other.id == id &&
other.date == date;
}
@override
int get hashCode => id.hashCode ^ date.hashCode;
}
ㅤ
Equatable 패키지를 사용했을 때 오히려 더 정확하게 동작해야 하는 것이 아닌가? 싶어서 Equatable의 내부 구현을 좀 찾아봤다.
ㅤ
대충 봤을 때, 내가 작성한 코드와 Equatable 의 코드 차이는 ==을 사용했냐? 아니면 identical 메서드를 사용했냐의 차이였다.
ㅤ
대체 어느 부분에서 문제가 발생한 건지 확인해보려고 직접 작성한 코드의 == 연산자 부분을 identical 로 바꾸고 디버거를 통해서 어떤 조건에서 false가 발생하는 건지 확인해봤다.
class MyUsecaseParam {
MyUsecaseParam({required this.id, required this.date});
final String id;
final DateTime date;
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is MyUsecaseParam && identical(id, other.id) && identical(date, other.date));
}
@override
int get hashCode => id.hashCode ^ date.hashCode;
}
ㅤ
그리고 튀어나온 문제는 아래와 같았다. 전혀 예상치 못했다. 전혀.
identical(date, other.date)
=> false
date == other.date
=> true
ㅤ
DateTime 자료형에 대한 Equatable 패키지의 identical과 == operator 의 비교 방식의 차이에서 문제가 발생하고 있었다. 사실 identical 이라는 메서드 자체를 처음봐서, 인터페이스 문서를 확인하려고 들어가보니 주석으로 정확하게 딱 적혀있었다. identical 메서드는 같은 레퍼런스를 가지고 있는지 확인합니다.
ㅤ
== 연산자의 경우에는 값이 동일한 지를 확인하고 있었고, identical은 참조가 동일한 지를 확인하고 있어서 문제가 발생한 것이였다. 나의 경우에는 생성된 여러 파라미터가 동일한 값을 가지고 있는지 검사하고 싶었던 거기 때문에 identical 이 아닌 == 을 사용해주어야 했고, 그러려면 Equatable 패키지가 어울리지는 않았던 것 같다.
ㅤ
(Equatable 패키지 문서에는 이런 내용은 없고 “인스턴스간의 비교가 간편해집니다!!” 이런 것들만 있었던 것 같은데,,, 내가 못봤던거겠지,,, 흠,,,)
ㅤ
요약하자면
- Equatable 패키지를 사용하면 인스턴스간의 비교를 쉽게 작성할 수 있음.
- 근데 내부적으로 identical 메서드를 사용하기 때문에, 원시타입이 아닌 객체의 비교의 경우에는 값 비교가 아니라 동일한 객체를 참조하는지 확인함.
- 값 비교가 필요할 때에는 == 을 이용해 직접 확인하자.
'Develop > Flutter 개발' 카테고리의 다른 글
[Flutter] FutureProvider의 캐싱 (0) | 2025.01.14 |
---|---|
[Flutter] 같은 열거 값 이름을 가진 서로 다른 두 Enum 간의 변환에는 findWhere을 사용하자! (0) | 2024.09.28 |
[Flutter] 3.24 버전부터 Swift Package Manager를 지원! (3) | 2024.09.28 |
[Flutter] Dart의 void 타입은 null이 아니다 / this function has a return type of void and cannot be used (0) | 2024.09.20 |
[Flutter] 플러터의 화면 렌더링 과정 (2) | 2024.07.24 |