[Flutter] Freezed 패키지를 활용한 data class 생성

pendant.kpendant.k
4 min read

들어가며

Flutter를 가지고 서버 통신 기능을 가진 앱을 개발을 하다보면 DTO(Data Transfer Object)를 정의해야할 일이 빈번하게 발생합니다. JSON을 받아와서 데이터 클래스로 바꾸기 위해서 factory를 선언하고, toString()을 만들어주는 등, 이것저것 해야할 일이 많기도 하고 반복적으로 발생하는 일이기도 합니다. 특히나 Union타입을 사용해야하는 경우에는 직접 관련된 로직을 구현해야해서 골치가 아프기도 하죠.(기본적으로 Dart에서는 union 타입을 지원하지 않습니다.)

Flutter 서드파티 패키지 중에서는 이런 상황에 아주 유용하게 활용할 수 있는 패키지가 존재합니다. 바로 Freezed라는 패키지 인데요. 기본적으로 data class를 만들기 위해서 필요한 노동을 code generator를 통해서 자동으로 생성해주는 패키지입니다.

이번 포스트에서는 Freezed 패키지와 기본적인 사용법에 대해서 다뤄보고자 합니다.


Freezed, data-class 코드 생성기

Freezed는 data-classes/unions/pattern-matching/cloning을 위한 코드 생성기입니다.

Freezed 공식문서에 나오는 정의입니다. 말그대로 data-class와 관련된 대부분의 작업을 자동으로 도와주는 패키지입니다. 저는 code generation을 해당 패키지를 쓰면서 처음 사용해보았기 때문에 조금 생소한 부분이 있긴 했습니다. 요즘에는 Riverpod 패키지에서도 적극적으로 코드 생성기를 사용하는 거 같습니다. 알아두면 개발 생산성 향상에 큰 도움이 되는 거 같습니다.

Freezed 패키지는 기본적으로 code generation을 활용하기 때문에 저희는 필요한 data class의 인터페이스 정도만 작성을 하면 알아서 이와 관련된 필요한 코드를 자동으로 생성해주게 됩니다.

Freezed 설치하기

앞서 언급했듯이 해당 패키지는 code generator를 적극 활용합니다. 따라서 build_runner와 code-generator에 대한 설정이 필요합니다.

dart pub add freezed_annotation
dart pub add --dev build_runner
dart pub add --dev freezed
# fromJson/toJson 생성도 사용하려면 아래를 추가
dart pub add json_annotation
dart pub add --dev json_serializable
  • build_runner : code-generators를 실행하기 위한 도구입니다. 터미널에서 build_runner를 실행해서 자동 생성 코드를 만드는 작업을 진행하게 됩니다. Freezed를 활용해 data class를 생성하면 다양한 기본 매서드를 제공하기 위한 코드가 추가적으로 자동 생성됩니다.

  • freezed : code generator 입니다.

  • freezed_annotation : freezed를 사용하기 위한 annotations를 포함하고 있습니다.

Freezed 코드 생성을 사용하려는 클래스에 패키지가 탑재한 자체적인 annotation을 지정해서 해당 클래스를 기반으로 코드를 생성하게 됩니다.

Freezed 사용법

freezed를 사용하기 위해서는 가장 먼저 generator를 실행해야 합니다.

flutter pub run build_runner build

해당 명령어를 실행해두면 변경 내용이 저장될 때마다 freezed annotation이 있는 부분을 찾아서 자동으로 필요한 코드를 생성하게 됩니다.

freezed를 사용하기 위해서는 annotation을 import해야하며, flutter/foundation.dart도 함께 import해서 사용하는 것이 좋습니다. 개체를 더 보기 좋게 만들어주기 때문에 Flutter의 devtool를 다룰 때 유용합니다.

공식 깃허브에서 제공하는 freezed를 사용한 모델은 다음과 같습니다. 긴 설명보다 기본적인 Freezed를 활용한 데이터 클래스의 코드를 살펴보며 이해해보도록 하겠습니다

// 이 파일은 "main.dart" 입니다.
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

// 필수: `main.dart`를 Freezed에서 생성한 코드와 연결합니다.
part 'main.freezed.dart';
// 옵션(선택사항): Person 클래스에 Json 직렬화를 지원하기 위해 다음의 파일도 연결해야합니다.
part 'main.g.dart';

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

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

위의 코드에서는 Person 이라는 모델을 정의하고 있습니다. 이 Person 클래스는 firstName, lastName, age 라는 세가지 프로퍼티를 가지고 있습니다.

  • 먼저 @freezed 어노테이션이 적용된 클래스이기 때문에 클래스의 모든 속성은 변경할 수 없습니다. (immutable)

  • 맨 아래쪽 코드에서 fromJson factory를 생성했기 때문에 Json 직렬화/역직렬화가 가능한 클래스입니다.

이렇게 간단한 코드만 작성하면 Freezed에서는 다음과 같은 기본적인 기능을 자동으로 생성해줍니다.

  • 객체의 속성을 수정하는 것 대신 다른 속성을 가진 객체를 복제하는 copyWith 매서드

  • 객체의 모든 속성을 나열하는 toString 매서드를 오버라이드합니다.

  • ==연산자 및 hashCode 를 재정의합니다.

Freezed를 사용할 때 반드시 주의해야할 점은 다음과 같습니다.

  • part 부분을 빼먹기가 쉬운데, 반드시 포함해야합니다. 정확하게 해당 부분을 작성해주어야 .g.dart 파일이 정상적으로 생성됩니다. (생성된 파일은 직접적으로 수정하면 안됩니다.) 처음에는 당연하게도 존재하지 않는 파일이기 때문에 에러가 발생합니다. build_runner를 실행해주면 해당 경로에 알맞는 *.g.dart 파일이 생성됩니다.

  • class 위에는 @freezed 어노테이션을 반드시 포함해야합니다. (@Freezed, @unfreezed를 사용하는 경우도 존재합니다.)

  • 작성한 class는 반드시 _$ 접두사가 붙은 클래스 이름과 함께 mixin해야합니다. 자동으로 생성되는 코드에서 _$ClassName을 사용합니다.

  • Freezed를 활용하는 class에서 기본 생성자를 선언할 때는 factory를 사용해야합니다.(const는 옵션입니다.)

Freezed없이 작성된 데이터 클래스와의 비교

만약 Freezed의 코드 생성 기능없이 위에서 언급한 기능을 모두 가진 data class를 손수 작성하려면 다음과 같은 코드가 필요합니다.

import 'package:flutter/foundation.dart';

@immutable
class Person {
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  final String firstName;
  final String lastName;
  final int age;

  // Json serialization 구현
  Map<String, dynamic> toJson() => {
        'firstName': firstName,
        'lastName': lastName,
        'age': age,
      };

  // Json deserialization 구현
  factory Person.fromJson(Map<String, dynamic> json) => Person(
        firstName: json['firstName'] as String,
        lastName: json['lastName'] as String,
        age: json['age'] as int,
      );

  // copyWith() 구현
  // 주로 immutable 클래스를 수정해야할 때 활용합니다.
  // Flutter에서도 TextStyle()같은 클래스에서 많이 활용합니다.
  Person copyWith({String? firstName, String? lastName, int? age}) {
    return Person(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
      age: age ?? this.age,
    );
  }

  // toString() override
  @override
  String toString() {
    return 'Person{firstName: $firstName, lastName: $lastName, age: $age}';
  }

  // == operator override
  // 기본적으로 정의된 == 연산자를 사용하면 두 객체의 value 대한 비교를 하지 않습니다.
  @override
  bool operator ==(Object other) =>
      other is Person &&
      runtimeType == other.runtimeType &&
      firstName == other.firstName &&
      lastName == other.lastName &&
      age == other.age;

  // hashCode override
  @override
  int get hashCode {
    return Object.hash(runtimeType, firstName, lastName, age);
  }
}

앞서 살펴본 Freezed 코드와 비교해보았을 때, 꽤나 많은 코드 작성이 필요하다는 걸 한눈에도 확인할 수 있습니다. 또한 하드 코딩으로 모든 데이터 클래스를 생성하고 이에 필요한 매서드를 직접 구현하다보면 실수가 발생할 수도 있기 때문에 Freezed를 활용하는 것을 추천드립니다.

Freezed는 이외에도 mutable 객체 정의, union 타입 등 더 다양한 기능을 제공합니다. 필요한 기능들은 공식 문서에서 확인할 수 있습니다.

참고 문헌

Freezed 공식 한글 문서

edited by 김동한

0
Subscribe to my newsletter

Read articles from pendant.k directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

pendant.k
pendant.k

iOS / Flutter Developer