본문 바로가기
Flutter/Package

플러터에서 Freezed 플러그인! Entity Code Generation은 이거 하나로 끝

by Maccrey Coding 2024. 10. 27.
728x90
반응형

 

Flutter는 코드 생성 기능이 매우 활성화되어 있습니다.

잘 알려진 json_serializable 라이브러리와 retrofitchopper 라이브러리도 이에 해당합니다.

오늘 소개할 freezed는 데이터 클래스에 다양한 편의 기능을 제공하는 코드 생성 라이브러리입니다.

Freezed vs Json Serializable

"Freezed 라이브러리가 이미 사용되고 있는 다른 코드 생성 라이브러리와 도대체 뭐가 다른가?"라는 질문이 생길 수 있습니다.

freezed는 데이터 클래스에서 필요한 기능들을 한 번에 제공해주는 라이브러리입니다.

비슷한 역할을 하는 json_serializable과 함께 사용하면, freezed는 copy 기능, toString 오버라이드, Union 클래스 등을 추가적으로 사용할 수 있게 해줍니다.

Freezed 사용하기

freezed를 사용하기 위해 필요한 의존성을 아래와 같이 추가해야 합니다.

dependencies:
  freezed_annotation: ^<latest_version>

dev_dependencies:
  build_runner: ^<latest_version>
  freezed: ^<latest_version>
  json_serializable: ^<latest_version>
  • json_serializable은 추가할 수도 있고 안 할 수도 있습니다. 만약 toJson 및 fromJson 기능을 사용하고 싶다면 반드시 추가해야 합니다.

기본 문법

freezed 패키지는 어노테이션 기능을 사용하여 코드 생성을 실행합니다. 일반적으로 클래스에 속성을 선언할 때, 클래스 내부에 변수를 미리 선언하고 생성자로 입력된 변수들을 클래스의 변수에 할당하지만, freezed는 팩토리 생성자를 사용하면 클래스 내부에 변수를 정의할 필요가 없습니다.

예제 코드

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';
part 'person.g.dart';

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

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

위 코드처럼 Person 클래스에 id, name, age 값을 저장하고 싶다면 해당 값들을 Person 팩토리 생성자에 정의하기만 하면 됩니다. JSON 직렬화 기능을 원하시면 part 'filename.g.dart'를 추가하고 fromJson 팩토리 함수만 제작하시면 됩니다.

 

이제 코드를 작성한 후 코드 생성을 실행해보겠습니다.

flutter pub run build_runner build

 

이제 이 짧은 코드가 제공하는 많은 기능을 살펴보겠습니다.

컨스트럭터 및 Property 자동 생성

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

// Property 접근
print(person1.id); // 1
print(person1.name); // Code Factory
print(person1.age); // 52
  • 자동으로 클래스 속성을 생성하여 작성해야 할 코드가 줄어듭니다.

toString 및 toJson

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

// 자동으로 형식을 지정한 toString() 결과
print(person1); // Person(id: 1, name: Code Factory, age: 52)

// JSON 형식으로 변환
print(person1.toJson()); // {id: 1, name: Code Factory, age: 52}
  • 일반적으로 Dart에서 클래스 인스턴스에 toString()을 실행하면 유용하지 않은 정보가 출력됩니다. 하지만 freezed는 toString() 메서드를 자동으로 오버라이드하여 디버깅 시 유용한 정보를 제공합니다.

== 및 hashCode 오버라이드

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

// true
print(person1 == person2); // true
  • freezed는 == 함수와 hashCode를 자동으로 오버라이드하여 메모리 위치가 아닌 필드 값으로 비교합니다.

Assert 하기

생성자에서 assert를 통해 변수 값을 제한하고 싶을 때 사용할 수 있습니다.

@freezed
class Person with _$Person {
  @Assert('name.length < 5', '이름은 5자 이하만 입력 가능합니다.')
  factory Person({
    required int id,
    required String name,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

// 에러 발생
final person1 = Person(id: 1, name: 'Code Factory', age: 52); // 이 오류 발생
  • 여러 개의 assert를 추가하고 싶으면 팩토리 생성자 위에 계속 추가하시면 됩니다.

커스텀 메서드 및 Getter 작성하기

freezed로 클래스를 생성해도 원하는 메서드 또는 getter를 추가할 수 있습니다. 단, 내부 생성자를 반드시 추가해야 합니다.

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

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  // 내부 생성자 추가
  Person._();

  int get nameLength => name.length;

  void sayHello() {
    print('hello');
  }
}

Copy

freezed는 기본적으로 클래스를 immutable하게 사용하기 때문에 setter를 설정하는 것은 불가능합니다.

하지만 일반적으로 copy 메서드를 정의하여 사용하게 되며, freezed는 이 기능도 자동으로 생성합니다.

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

final person2 = person1.copyWith(id: 2);

// Person(id: 2, name: Code Factory, age: 52)
print(person2);

Deep Copy

freezed는 Deep Copy 기능을 간단하게 제공합니다. 여러 클래스를 네스팅해 보겠습니다.

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

@freezed
class Group with _$Group {
  factory Group({
    required int id,
    required String name,
    required School school,
  }) = _Group;
}

@freezed
class School with _$School {
  factory School({
    required int id,
    required String name,
  }) = _School;
}

// 객체 생성
final school1 = School(id: 3, name: 'Harvard');
final group1 = Group(id: 2, name: 'Coding Group', school: school1);
final person1 = Person(id: 1, name: 'Code Factory', age: 52, group: group1);

freezed 패키지를 사용하지 않았다면, 다음과 같이 복잡한 코드를 작성해야 했을 것입니다.

final person2 = person1.copyWith(
  group: group1.copyWith(
    school: school1.copyWith(name: 'Stanford'),
  ),
);

// person2.group.school.name 만 Stanford 로 변경
print(person2);

하지만 freezed의 Deep Copy 기능을 사용하면 코드가 훨씬 간단해집니다.

final person3 = person1.copyWith.group.school(name: 'Stanford');

// person3.group.school.name 만 Stanford 로 변경
print(person3);

이렇게 훨씬 적은 코드로도 가능합니다. 이는 캐싱 관련 작업을 할 때 매우 유용합니다.

Union

freezed의 Union 기능을 사용하여 간단히 내부 클래스를 정의하고, 각 생성자별로 다른 클래스 인스턴스를 반환할 수 있습니다.

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

  factory Person.loading({int? statusCode}) = _Loading;

  factory Person.error(String message, {int? statusCode}) = _Error;
}

final person = Person(id: 1, name: 'Code Factory', age: 52, statusCode: 200);
final personLoading = Person.loading();
final personError = Person.error('failed to fetch', statusCode: 401);

Union 인스턴스 사용 예시

print(person.statusCode); // 200
print(personLoading.statusCode); // null
print(personError.statusCode); // 401

각 컨스트럭터에서 공통으로 제공하는 변수는 직접 가져올 수 있습니다. 각기 특화된 컨스트럭터에서 제공하는 파라미터는 when, maybeWhen, map, maybeMap 등을 사용하여 불러올 수 있습니다.

when 사용하기

when 메서드는 각 타입에 따라 다른 동작을 수행하도록 해줍니다.

void generalizeWhen(Person person) {
  person.when(
    (id, name, age, statusCode) =>
        print('id: $id name: $name age: $age statusCode: $statusCode'),
    loading: (int? statusCode) => print('loading'),
    error: (String message, int? statusCode) => print('error : $message'),
  );
}

final person = Person(id: 1, name: 'Code Factory', age: 52, statusCode: 200);
final personLoading = Person.loading();
final personError = Person.error('failed to fetch', statusCode: 401);

// id: 1 name: Code Factory age: 52 statusCode: 200
generalizeWhen(person);

// loading
generalizeWhen(personLoading);

// error : failed to fetch
generalizeWhen(personError);

maybeWhen, map, maybeMap 사용하기

  • maybeWhen: when과 비슷하지만, 선택적으로 행동할 수 있습니다. 제공된 매개변수가 없는 경우에는 기본 동작을 정의할 수 있습니다.
void generalizeMaybeWhen(Person person) {
  person.maybeWhen(
    error: (message, statusCode) => print('Error occurred: $message'),
    orElse: () => print('Not an error'),
  );
}

// 예시
generalizeMaybeWhen(personError); // Error occurred: failed to fetch
generalizeMaybeWhen(person); // Not an error
  • map: 각각의 타입에 대해 다른 함수를 실행합니다.
void generalizeMap(Person person) {
  person.map(
    person: (value) => print('Person: ${value.name}'),
    loading: (value) => print('Loading...'),
    error: (value) => print('Error: ${value.message}'),
  );
}

// 예시
generalizeMap(person); // Person: Code Factory
generalizeMap(personLoading); // Loading...
generalizeMap(personError); // Error: failed to fetch
  • maybeMap: map과 유사하나, 해당하는 타입이 없을 때 기본 동작을 정의할 수 있습니다.
void generalizeMaybeMap(Person person) {
  person.maybeMap(
    error: (value) => print('Error: ${value.message}'),
    orElse: () => print('Not an error'),
  );
}

// 예시
generalizeMaybeMap(personError); // Error: failed to fetch
generalizeMaybeMap(person); // Not an error

이처럼 freezed는 Flutter에서 데이터 클래스를 관리하고 처리하는 데 많은 편리함과 유연성을 제공합니다. 각 기능을 잘 활용하면 더 간결하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

구독!! 공감과 댓글,

광고 클릭은 저에게 큰 힘이 됩니다.

 

Starting Google Play App Distribution! "Tester Share" for Recruiting 20 Testers for a Closed Test.

 

Tester Share [테스터쉐어] - Google Play 앱

Tester Share로 Google Play 앱 등록을 단순화하세요.

play.google.com

 

 

728x90
반응형