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

Written by 코드팩토리 JC

1월 15, 2024

Flutter Freezed

서론

Flutter 는 Code Generation 기능이 상당히 많이 활성화되어 있어요. 흔히들 많이 사용하는 json_serializable 라이브러리도 있고 retrofit 및 chopper 라이브러리도 있습니다. 오늘 알려드릴 freezed 또한 데이터 클래스에 편의 기능들을 제공해주는 code generation 라이브러리입니다.

Freezed vs Json Serializable

Code Generation 이라는 말을 들었을때 “Freezed 라이브러리가 이미 흔히 사용되고있는 다른 Code Generation 라이브러리와 도대체 뭐가 다른데 또 Code Generation 라이브러리가 필요하지?” 라는 생각이 들 수 있습니다. Freezed 라이브러리는 데이터 클래스에서 흔히 필요한 기능들을 한번에 제공해주는 라이브러리입니다. 이런 비슷한 계얼에 jsonserializable 이 있는데 jsonserializable 과 혼합해서 freezed 는 copy 기능, toString override, union 클래스 등 필요한 편의성 기능들을 추가로 사용할 수 있게 해줍니다.

Freezed 사용하기

Freezed 를 사용하기 위해서는 아래 디펜던시들을 추가해야합니다.

dependencies:
  freezed_annotation:

dev_dependencies:
  build_runner:
  freezed:
  json_serializable:
YAML

jsonserializable 은 추가해도되고 안해도 상관은 없는데 만약에 toJson 기능과 fromJson 기능을 사용하고 싶다면 jsonserializable 을 추가해야합니다.

기본 문법

Freezed 패키지는 annotation 기능을 사용해서 Code Generation 기능을 실행합니다. 일반적으로 클래스에 property 를 선언할때 클래스 내부에 변수들을 미리 선언을 해두고 constructor 로 입력된 변수들을 클래스의 변수들에 입력을 해주게되는데 Freezed 의 경우에는 factory constructor 를 사용하면 클래스 내부에 변수정의를 따로 해줄 필요가 없습니다. 자동으로 mixin 을 통해 code generation 이 되기 때문이죠.

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);
}
Dart

위 코드와 같이 Person 클래스에 id, name, age 값들을 저장하고 싶다면 해당 값들을 Person factory constructor 에 정의를 해주기만 하면됩니다. 추가적으로 json_serializable 의 fromJson 및 toJson 기능을 사용하고 싶으시다면 part 'filename.g.dart 를 추가하시고 fromJson factory 함수만 추가로 제작해주시면 됩니다.

원하는 코드를 작성하고 나서 code generation 을 실행해주시면 되겠습니다

flutter pub run build_runner build
ShellScript

이제 이 짧은 코드가 어떤 수많은 기능들을 제공해주게 되는지 확인해보겠습니다.

컨스트럭터 및 property 자동생성

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

// 1
print(person1.id);

// Code factory
print(person2.name);

// 52
print(person3.age);
Dart

위와같이 자동으로 클래스 property 들을 제작해줘서 작성해야 할 코드가 적어집니다.

toString 및 toJson

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

// Person(id: 1, name: Code Factory, age: 52)
print(person1);

// {id: 1, name: Code Factory, age: 52}
print(person1.toJson());
Dart

일반적으로 Dart 언어에서 클래스 인스턴스에 toString() 함수를 실행시키면 Instance of {클래스} 이런식으로 유용하지 않은 정보가 리턴됩니다. 이 부분은 toString 메소드를 override 하면서 조금 더 중요한 정보들을 제공해주는 형태로 변경이 가능한데 프로젝트가 커질수록 상당히 귀찮고 관리하기 어려운 작업이 됩니다. 하지만 freezed 를 사용하면 toString() 메소드가 자동으로 override 되어서 debugging 에 상당히 유용합니다.

== 및 hashCode override

freezed 는 == 함수 및 hasCode 함수 또한 자동으로 override 합니다. 글래스 인스턴스를 특별한 override 없이 서로 비교하게되면 메모리 위치를 서로 비교하게 됩니다. 결과적으로 같은 클래스의 인스턴스고 모든 필드가 다 같더라도 비교는 false 가 나오게되죠. 하지만 freezed 를 사용하면 자동으로 클래스의 모든 property 의 조합으로 == 및 hashCode 함수가 override 되어서 상식적인 비교를 진행할 수 있습니다.

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

// true
print(person1 == person2);
Dart

Assert 하기

class 컨스트럭터를 제작할때 assert 를 통해서 변수 값을 제한하고싶을때가 있습니다. freezed 패키지도 assert 기능을 사용할 수 있도록 annotation 을 따로 제공해주고 있습니다.

@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);
}

// 에러 -> 이름은 5자 이하만 입력 가능합니다.
final person1 = Person(id: 1, name: 'Code Factory', age: 52);
Dart

Assert 키워드를 통해서 String 값으로 코드를 작성해야 하는게 아쉽긴 하지만 보통 assert 조건은 까다롭게 작성되는 경우가 잘 없기때문에 크게 문제가 될 것 같지는 않습니다. 여러개의 Assert 를 작성하고싶으면 factory 컨스트럭터 위에 계속 추가하시면 됩니다.

커스텀 method 및 getter 작성하기

freezed 패키지로 class 를 생성해도 당연히 원하는 method 또는 getter 를 작성할 수 있습니다. 하지만 class 에 한줄의 internal constructor 를 필수적으로 추가 해줘야합니다.

@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);

  // 메소드나 custom getter 작성시 필수로 추가
  Person._();

  get nameLength => name.length;

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

위와같이 internal constructor 를 하나만 추가해주면 일반적으로 저희가 사용하는대로 custom getter 와 method 를 작성할 수 있습니다.

Copy

freezed 는 기본적으로 class 를 immutable 하게 사용하는걸 목적으로 하기때문에 setter 를 설정하는건 불가능합니다. 하지만 여느 OOP 언어와 같이 일반적으로 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);
Dart

Deep Copy

freezed 패키지는 deep copy 기능또한 간단하게 제공합니다. 아래처럼 여러 클래스를 nesting 해보도록 하겠습니다.

@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);
Dart

만약에 person1 변수의 school 을 변경하고싶으면 freezed 패키지 없이 copyWith 만 존재할때를 가정했을대 아래와 같은 코드를 작성해야합니다.

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

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

하지만 freezed 의 deep copy 기능을 사용하면 이렇게 복잡한 nesting 을 할 필요가 없습니다.

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

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

훨씬 더 적은 코드를 작성해도 되고 편리하죠? Caching 관련 작업을 할때 상당히 유용한 기능들입니다.

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);

// 200
print(person.statusCode);


// null 
print(personLoading.statusCode);

// 401 
print(personError.statusCode);
Dart

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

generalizeWhen(Person person) {
  return 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);
Dart

maybeWhen, map, maybeMap 사용법은 영상을 참조해주세요!

관련 포스트

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

서론 불변 프로그래밍: 현대 개발의 핵심 현대 소프트웨어 개발에서 불변 프로그래밍(Immutable Programming)의 중요성은 간과할 수 없는 요소입니다. 플러터(Flutter)에서도 마찬가지로 불변 프로그래밍 개념이 매우 중요하며, copyWith 함수는 이러한 불변성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 플러터를 배우기 시작하는 개발자들에게 불변 프로그래밍의 중요성을 강조하고, copyWith 함수의 역할과 사용 방법에 대해 설명 해보겠습니다!...

ChatGPT가 이야기하는 2024년 개발자 로드맵

ChatGPT가 이야기하는 2024년 개발자 로드맵

서론 개발자의 여정을 시작하며 안녕하세요, 미래의 개발자 여러분! 오늘부터 시작하는 여러분의 개발 여정에 함께할 수 있어서 기쁩니다. 2023년은 기술이 매우 빠르게 변화하는 해였으며, 이러한 변화 속에서 개발자가 되기 위한 길은 더욱 다채롭고 흥미로워졌습니다. 이 로드맵은 초보자인 여러분이 개발의 세계에 첫발을 내딛는 데 필요한 기초부터 시작해, 점차 심화 단계로 나아가는 길을 안내해 드릴 것입니다. 백엔드 개발 이 글은 단순히 기술을 배우는 것 이상의 의미를 가집니다....

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

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

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