View

300x250

어제부터 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 패키지 문서에는 이런 내용은 없고 “인스턴스간의 비교가 간편해집니다!!” 이런 것들만 있었던 것 같은데,,, 내가 못봤던거겠지,,, 흠,,,)

요약하자면

  1. Equatable 패키지를 사용하면 인스턴스간의 비교를 쉽게 작성할 수 있음.
  2. 근데 내부적으로 identical 메서드를 사용하기 때문에, 원시타입이 아닌 객체의 비교의 경우에는 값 비교가 아니라 동일한 객체를 참조하는지 확인함.
  3. 값 비교가 필요할 때에는 == 을 이용해 직접 확인하자.
320x100
Share Link
reply
반응형
«   2025/01   »
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 30 31