[Flutter] Equatable 플러그인, 도대체 어디에 쓰는건가요?

서론

안녕하세요 코드팩토리입니다. 오늘은 Equatable 플러그인 사용법에대한 강의를 가져왔어요. 외부 플러그인들을 보거나 튜토리얼을 보면 클래스들이 Equatable 부모 클래스를 익스텐드하는걸 자주 볼 수 있는데 커뮤니티에서도 종종 질문이 들어와서 왜 Equatable 플러그인을 사용해야하는지 설명을 해보려고 합니다.

Equatable 이란?

Equatable 플러그인은 한 인스턴스와 다른 인스턴스가 같은 인스턴스인지 판단을 쉽게 할 수 있게 해주는 플러그인입니다. 일반적으로 인스턴스를 두번 생산하고 이 둘을 '==' 로 비교를 하면 항상 false 가 나옵니다. 왜냐면 primitive 값이 아닌이상 값을 베이스로 비교를 하는게 아니라 메모리의 위치를 기반으로 비교를 하게됩니다. 예를들어 만약에 new Person(name:'CF') == new Person(name:'CF') 라는 코드를 작성했다면 따로 equality 에 대한 정의를 하지 않는이상 절대적으로 false 값이 나옵니다.

해법

OOP 라는 명칭이 생겨난 이유는 모든 클래스는 기본적으로 Object 를 extend 하기 때문입니다. 이 뜻은 Object 라는 최상위 클래스가 제공해주는 모든 파라미터들을 모든 클래스들이 사용할 수 있다는 뜻도 되죠. 대부분의 언어에서는 이 Object 라는 클래스에 한 인스턴스와 다른 인스턴스를 비교하는 알고리즘이 정의가 되어있습니다. Dart 언어에서는 operator 라는 함수에 정의가 되어있고 이를 override 를 함으로서 값의 비교 알고리즘을 자유롭게 변경할 수 있습니다.

Operator 함수 override 하기

class Person {
  final int id;
  final String name;
  final int age;

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

  
  bool operator ==(Object other) {
    return other is Person && other.id == this.id;
  }

  
  int get hashCode {
    return this.id;
  }
}

위와같이 Person 클래스를 제작하면 id 를 기반으로 Person 클래스의 인스턴스들을 비교할 수 있습니다.

final person1 = Person(id: 1, name: 'Code Factory', age: 99);
final person2 = Person(id: 1, name: 'Code Factory 2', age: 99);

print(person1 == person2);

위 코드에서 person1 과 person2 는 같다는 결과가 나옵니다. name 이나 age 파라미터를 어떻게 바꿔도 Person 클래스는 id 값으로만 비교하게 operator == 함수를 정의했기 때문입니다.

hashcode 함수

operator == 함수는 클래스의 equality 를 비교하는데 사용된다는걸 이제 알았습니다. 그럼 hashcode 함수는 어디에 사용하는걸까요? Hashcode 는 Map 또는 Set 에서 키의 역할을 하게됩니다. Map 이나 Set 은 키가 중복으로 저장될 수 없기때문에 Set 이나 Map 의 키로 오브젝트가 저장되었을때 어떻게 키값을 정의할지가 중요합니다. 저희가 정의한 Person 클래스에서는 id 를 기반으로 키값을 정의하였고 그렇기때문에 아래 코드에 person1 과 person2 를 map 에 넣어도 map 의 길이는 1로 나옵니다. Id 가 같으면 같은 hashcode 가 생성되게 정의를 했기때문이죠.

final person1 = Person(id: 1, name: 'Code Factory', age: 99);
final person2 = Person(id: 1, name: 'Code Factory 2', age: 99);

final map = {
  person1: true,
  person2: true,
};

print(map.length);

Equatable 이 필요한 이유

지금 Person 클래스는 id 만을 사용해서 operator == 과 hashcode 메소드를 정의하고 있습니다. 하지만 만약에 모든 값들을 비교 범위에 포함시키고싶으면 어떻게 해야할까요?

class Person {
  final int id;
  final String name;
  final int age;

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

  
  bool operator ==(Object other) {
    return other is Person && other.id == this.id 
      && other.name == this.name 
      && other.age == this.age;
  }

  
  int get hashCode {
    return this.id;
  }
}

이런식으로 operator == 메소드를 변경해야합니다. 파라미터가 많아질수록 작성해할 코드도 길어지겠죠. hashcode 또한 문제입니다. 절대적으로 int 값을 돌려줘야하는데 String 같은 int 로 환산되기 어려운 값들은 어떻게 정의 해줘야할까요?

이 문제를 해결해주는게 바로 Equatable 라이브러리입니다.

사용법

Equatable 이 왜 필요한지 이해만 하고나면 Equatable 함수의 사용법은 상당히 쉽습니다. Equatable 클래스를 상속을 받고 props 라는 메소드를 override 해주면 됩니다. props 메소드는 리스트를 받게되는데 이 리스트에 들어가는 모든 값들의 조합이 operator == 과 hashcode 함수를 생성하는데 사용됩니다.

class Person extends Equatable{
  final int id;
  final String name;
  final int age;

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

  
  List<Object> get props => [this.id, this.name, this.age];
}

굉장히 간단하죠? 위 코드를 작성한 뒤 아래 비교를 실험해보면 저희가 예상하는 값들이 나오는걸 볼 수 있습니다.

final person1 = Person(id: 1, name: 'Code Factory', age: 99);
final person2 = Person(id: 1, name: 'Code Factory 2', age: 99);
final person3 = Person(id: 1, name: 'Code Factory', age: 99);

/// False
print(person1 == person2);

/// True
print(person1 == person3);

어떤가요? 편리하신가요? 안써도 그만이긴 하지만 한번 사용하기 시작하면 몸에 금방 베기는 플러그인입니다!

©Code Factory All Rights Reserved