flutter | 상태관리에서 놓치기 쉬운 객체 동등성

두더지·2025년 7월 1일
post-thumbnail

1. 서론

일단, Object equality를 비교하기 전에 Dart의 타입 시스템, 특히 finalconst의 차이를 명확히 이해하는 것이 중요하다. Dart 문법 공부 처음할 땐 대충 넘겼지만ㅎㅎ(뭐.. 하다보니 깨달을 수도 있는거죠..그냥 api 받아와서 UI그리면 그만인 줄 알았어요)

1.1 짚고 넘어가야하는 이유

  • 외부에서 들어온 데이터를 ListState로 관리할 때, 객체의 값은 같은데 == 비교가 false가 나오면?
  • 불필요한 setState() 발생, 위젯 리빌드, 캐시 미스 등의 문제가 생긴다.
  • 즉, 객체가 같다고 판단되게 하려면 단순히 만 같아선 안 되고, 객체 비교 기준을 직접 정의해야 한다.

1.2 final과 const의 차이점 비교

구분finalconst
선언 시점런타임에 결정됨컴파일 타임에 결정됨
인스턴스 생성매번 새 객체같은 값이면 재사용 (싱글턴처럼)
메모리각각 따로 있음공유됨 (정적 메모리로 할당됨)
동일성 (== 또는 identical)값이 같아도 false값이 같으면 true

다음과 같은 예제를 살펴보자

  • 먼저 Person 객체를 정의한다.
  • Person 클래스의 property는 id, name, email이다.

1.3 person.dart (person 객체 생성)

// ignore_for_file: public_member_api_docs, sort_constructors_first
class Person {
  final int id;
  final String name;
  final String email;

  Person({required this.id, required this.name, required this.email});

  
  String toString() => 'Person(id: $id, name: $name, email: $email)';

  Person copyWith({int? id, String? name, String? email}) {
    return Person(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }
}

person_page.dart

import 'package:dart_class/models/person.dart';
import 'package:flutter/material.dart';

class PersonPage extends StatelessWidget {
  const PersonPage({super.key});

  
  Widget build(BuildContext context) {
    final person1 = Person(id: 1, name: 'John', email: 'john@gmail.com');
    final person2 = person1.copyWith(id: 2, email: 'Johndoe@gmail.com');
    final person3 = Person(id: 1, name: 'John', email: 'john@gmail.com');

    print(person1);
    print(person2);

    print(person1 == person3);
    print(person1.hashCode);
    print(person3.hashCode);

    return Scaffold(appBar: AppBar(title: Text('Person')));
  }
}

출력 결과:
false
949309463
581349880

  • 현재 person1person3property는 같은 상태이다. 하지만 equality 연산자의 값이 false임을 확인할 수 있다.
  • dart의 기본 equality는 referential equality이다. 즉, 두 object가 메모리상 같은 곳을 가르킬 때, equality -> true 라고 반환하게 된다.
  • person1person3의 hash값이 다른걸 보아,, 다른 메모리에 할당되었음을 알 수 있다.
  • 고로 다른 존재.

결론적으로, object의 프로퍼티가 같을 때 논리적 동등성을 가지기 위해서는 hash값을 override하던지, 아니면 처음부터 불변값으로 정해야한다.

2. 본론

2.1. Construct의 type을 const로 지정, const 인스턴스 사용

person.dart (const 생성자)

const Person({required this.id, required this.name, required this.email});

person_page.dart

import 'package:dart_class/models/person.dart';
import 'package:flutter/material.dart';

class PersonPage extends StatelessWidget {
  const PersonPage({super.key});

  
  Widget build(BuildContext context) {
    const person1 = Person(id: 1, name: 'John', email: 'john@gmail.com');
    final person2 = person1.copyWith(id: 2, email: 'Johndoe@gmail.com');
    const person3 = Person(id: 1, name: 'John', email: 'john@gmail.com');

    print(person1);
    print(person2);

    print(person1 == person3);
    print(person1.hashCode);
    print(person3.hashCode);

    return Scaffold(appBar: AppBar(title: Text('Person')));
  }
}

출력결과

true 423981177 // person 1 == person3의 논리적 equality

2.2. hash값 override

  • vscode extension -> 객체 생성 도와주는 Tool

  • Generate equality클릭

// ignore_for_file: public_member_api_docs, sort_constructors_first
class Person {
  final int id;
  final String name;
  final String email;

  const Person({required this.id, required this.name, required this.email});

  
  String toString() => 'Person(id: $id, name: $name, email: $email)';

  Person copyWith({int? id, String? name, String? email}) {
    return Person(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }

  
  bool operator ==(covariant Person other) {
    if (identical(this, other)) return true;

    return other.id == id && other.name == name && other.email == email;
  }

  
  int get hashCode => id.hashCode ^ name.hashCode ^ email.hashCode;
}

2.3. equatable 패키지 사용

  • pub.dev equatable 링크
  • == 연산자와 hashCode를 직접 override하는 대신,Equatable을 상속받고 props만 정의해주면 된다.
  • 내부적으로는 props 리스트에 들어간 값들을 기준으로 == 비교와 hashCode 생성을 자동 처리한다.
  • 결국 hash값을 override하는 방식은 동일하지만, 훨씬 간결하다
import 'package:equatable/equatable.dart';

// ignore_for_file: public_member_api_docs, sort_constructors_first
class Person extends Equatable {
  final int id;
  final String name;
  final String email;

  const Person({required this.id, required this.name, required this.email});

  
  String toString() => 'Person(id: $id, name: $name, email: $email)';

  Person copyWith({int? id, String? name, String? email}) {
    return Person(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }

  
  List<Object> get props => [id, name, email]; 
}
  • props 정의할 때

  List<Object> get props => [id]; 

로 정의하면, id이 같지만 나머지 property가 다른 객체들의 동등성이 부여된다.!

3. 마무리

쉬운게 하나도 없다.

profile
일단 하긴 합니다.

0개의 댓글