Freezed Package : Data Class 만들기— (1)

Hong JongPyo
7 min readJan 7, 2022

Flutter가 2.8버전으로 업그레이드 되면서 Freezed가 ‘Flutter Favorite’ 딱지를 얻게 되었습니다.

Remi의 팬이며, 상태관리를 위해 Freezed와 Riverpod을 주로 사용하고 있는 저로서는 굉장히 기분좋은 소식이네요.

제가 처음 이 패키지를 접했을 때 아무런 배경지식없이 적용해보려고 하다가 굉장히 고생을 했었던 경험이 있습니다.

이번 시리즈의 목표는 Freezed 패키지를 사용할 때 알고있으면 좋겠다고 생각되는 배경 지식과 아주 기본적인 사용법을 설명하는 것이 목표입니다.

Freezed를 이해하기 위한 배경지식

저는 공부하면서 코틀린에서 제공하는 기능들과 비슷한 점이 굉장히 많다고 느꼈습니다.

그래서 코틀린의 세 가지 기능을 Freezed 패키지를 사용하는데 불편함이 없을 정도로만 설명하겠습니다.

  • Data Class
  • Sealed Class (Union)
  • when

이 글에서는 Data Class에 대해서 다루도록 하겠습니다.

Data Class

코틀린의 데이터 클래스는 모든 클래스가 정의해야하는 메소드들을 자동 생성해줍니다.

모든 클래스가 정의해야하는 메소드:

  • 객체의 문자열 표현을 도와주는 toString()
  • 객체의 동등성을 알려주는 equals() ( ==)
  • hash 컬렉션에서 비교를 도와주는 hashCode()

추가적으로 Data Class에서 생성해주는 메소드:

  • 불변성을 위해 객체를 (변경하고) 복사해주는 copy()

그 외에도 Data Class에서 자동 생성해주는 메소드는 많지만, Freezed를 사용하는데는 필요 없기 때문에 생략하겠습니다.

그럼 왜 이 메소드들이 재정의 되어야하며, 자동으로 생성해주는 것이 왜 필요한지 예제를 통해 보여드리겠습니다.

toString()

주로 디버깅과 로깅할 때 이 메소드를 사용합니다.

다른 사람이 만들어놓은 패키지의 클래스를 print에 넣어서 호출해봤을 때
Instance of ‘클래스 명’ 이런식으로 나와서 디버깅할 때 곤란/귀찮았던 적이 다들 한번씩은 있을거라고 생각합니다.
toString을 재정의 하는 것으로 디버깅할 때 필요한 정보를 String으로 호출할 수 있도록 만들 수 있습니다.

equals() (==)

서로 다른 두 객체가 내부에 동일한 데이터를 포함하는 경우 그 둘을 동등한 객체로 봐야할 때가 있습니다. 그럴 때 equals를 재정의할 필요가 있습니다.

예를들어 유저 객체를 만들어 보겠습니다.

class User {

User({ required this.nickname,required this.phone});

String nickname;
String phone;

// 예시를 위해 == operator만 재정의
// 원래는 hashCode도 재정의하는 것을 추천!
operator ==(Object other){
return other is User &&
other.phone == phone;
}

}

전화번호가 같다면 닉네임이 다르더라도 같은 유저로 봐야할 수 있습니다.
이럴 때 휴대전화번호가 같으면 true 반환하도록 equals를 재정의해서 사용합니다.

hashCode

hashCode는 평소에 사용할 일이 적어서 왜 필요한지 모를수도 있습니다.
이 메소드는 hash를 사용하는 컬렉션과 관련이 있습니다.

List같은 경우는 contains 메소드를 사용할 때 단순하게 ==를 사용해서 객체의 동등성 검사만 진행하지만
HashSet이나, HashMap의 key 같은 경우는 hashCode를 우선적으로 검사한 뒤, 객체의 동등성을 검사합니다.

위의 다트패드에서 테스트 해보시길 바랍니다.

Copy()

우선 불변 객체가 왜 필요한지 이해해야 합니다.

hash를 사용하는 HashMap이나 HashSet의 경우는 불변성이 필수적입니다.

중복되는 값을 추가하지는 못하지만, 이미 추가된 객체를 변경시켜서 중복되게 만들 수 있습니다. 이렇게 되면 완전히 망가질 수 있습니다.

또한 다중 스레드 프로그램의 경우 사용 중인 데이터를 다른 스레드가 변경할 수 없기 때문에 스레드를 동기화할 필요가 줄어들기도 한다고 합니다.

그러나 이렇게 되면 불변객체를 변경해야할 때는 어떻게 해야되는지 의문점이 생깁니다.

그때 Copy 메소드를 이용해 일부 프로퍼티를 변경하면서 복사본을 만들어서사용합니다.

Flutter에서 Freezed를 이용해서 Data Class 구현하기

pubspec.yaml

dependencies:
freezed: ^1.1.1
freezed_annotation: ^1.1.0

dev_dependencies:
build_runner: ^2.1.7

data_class.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'data_class.freezed.dart';

@freezed
class Person with _$Person {
const factory Person({required String name,required int age}) = _Person;
}

터미널에 아래와 같은 명령어를 입력해줍니다.

flutter pub run build_runner build << 플러터로 진행중이라면

dart run build_runner build << 다트로 진행중이라면

main.dart

void main(List<String> arguments) {
var aPerson = Person(name: '홍종표', age: 26);
print(aPerson); // toString: Person(name: 홍종표, age: 26);
var bPerson = Person(name: '홍종표', age: 26);
print(aPerson == bPerson); // true
print(aPerson.hashCode == bPerson.hashCode); // true
aPerson = aPerson.copyWith(age: 27);
print(aPerson); // Person(name: 홍종표, age: 27)
}

직접 작성한 코드 7줄로 toString(), ==, hashCode, copyWith기능을 가진 클래스가 완성되었습니다.

만약 직접 구현했다면 꽤 길어집니다.

@immutable
class Person {
final String name;
final int age;

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

@override
String toString() {
return "Person(name: '$name', age: $age)";
}

@override
int get hashCode => name.hashCode ^ age.hashCode;

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

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

필드가 2개밖에 없는데도 기본 메소드를 작성하면 20줄이 넘기때문에 Freezed를 사용한다면 엄청난 생산성 향상을 가져올 수 있습니다.

--

--