본문 바로가기
IT-dev

플러터 macros, data class에 관해

by 김현진_ 2024. 7. 29.

24년 7월기준, 플러터에는 macros 라는 아주아주 큰 문법이 실험적으로 추가되었습니다
개인적으로는 호들갑 떨만큼 큰 기능이라 생각되어 글을 쓰게 되었습니다.

새로운 프로젝트를 시작하신다면,
@json_annotation 등의 code gen 사용을 한번 더 검토하세요


ㅎㅎ.. 조금 과한 오버였을까요? 아무튼 제가 왜 그렇게 생각하는지
다음 원문들을 정리도 하고 조금 정리하여 글을 써보도록 하겠습니다.
https://dart.dev/language/macros#the-macros-language-feature
https://github.com/dart-lang/language/blob/main/working/macros/motivation.md
 

1. 필요성 - macros 는 왜 필요한가?

(1) Data Class의 필요성

깃허브 커뮤니티에서는 꾸준하게 data class 를 요구해왔습니다. 파이썬, 자바, 코틀린에서 매우 유용하게 사용되고 있는 클래스이지만, 플러터-다트에서는 구현이 안되어 있습니다.
equatable을 사용하거나  ==, copyWIth 등을 사용해보신 경험들이 다들 있으실 겁니다. 매번 클래스마다 따로 정의를 해주어야하는게 여간 불편한건 아니었죠. 

데이터 클래스(Data Class)는 주로 데이터를 저장하는 용도로 사용되는 클래스입니다. 데이터 클래스는 일반적으로 데이터를 저장하는 속성(attribute)을 갖고 있으며, 이 데이터와 관련된 작업을 쉽게 수행할 수 있는 다양한 메서드를 자동으로 제공합니다. 데이터 클래스의 주요 특징은 다음과 같습니다:

  1. 자동 메서드 생성: 데이터 클래스는 __init__, __repr__, __eq__, 때로는 __hash__ 같은 공통 메서드를 자동으로 생성해줍니다. 이를 통해 인스턴스 초기화, 문자열 표현, 비교 작업 등을 쉽게 처리할 수 있습니다.
  2. 불변성: 일부 데이터 클래스는 기본적으로 불변(immutable)입니다. 즉, 한 번 인스턴스가 생성되면 그 데이터는 변경할 수 없습니다. 예를 들어, Kotlin에서는 데이터 클래스가 기본적으로 불변입니다.
  3. 비교 및 문자열 표현: 데이터 클래스는 인스턴스 간 비교 및 문자열 표현을 쉽게 도와주는 메서드를 제공합니다. 이를 통해 데이터 객체의 컬렉션을 다루거나 디버깅할 때 유용합니다.
    -chatGPT-

(2) 반복적인 코드

먼저 아래 코드를 보시면 다트는 "Foo"라는 위젯클래스를 정의하려면 무려 6번의 "Foo"를 써야합니다.
비효율적이고, 가독성이 떨어지며 개발자를 힘들게 합니다

class Foo extends StatefulWidget {
  Foo({ Key key, this.fixedStuff }) : super(key: key);
  FixedStuff fixedStuff;
  State<Foo> createState() => _FooState();
}

class _FooState extends State<Foo> {
  StatefulStuff statefulStuff = ...;
  @override
  Widget build(BuildContext context) {
    buildCode();
  }
}

(3) Json 대응, 빌드 간소화

기존 플러터 커뮤니티는 Json 데이터를 클래스로 읽어오기 위해 많은 노력을 했습니다.
@freezed, @JsonAnnotation 등  패키지를 활용했지만, json을 대응하는 클래스를 포함한 g.dart 파일을 만들기 위해  build_runner를 통해서 빌드 전 하나의 작업이 더 필요했습니다.

/// g.dart 파일을 생성하기 위한 build_runner 명령어

dart run build_runner build
g.dart는 이제 그만...


이 외에도 단점들이 많고 할말은 많지만 아래 블로그에 잘 정리되어 있어서 관심있으신 분은 읽어보시는 것을 추천드립니다. https://nx006.tistory.com/69

코드 제너레이션(Code Generation)

최근, 한 작은 Flutter 커뮤니티에서, 코드 제너레이션(Code Generation)에 대한 논쟁이 있었습니다. 그래서 이번 글에서는 이 논쟁에서 제시된, 코드 제너레이션을 사용해도 되는지에 대한 갑론을박을

nx006.tistory.com

 

2. 목표 - 플러터-다트의 개선방향

위와 같은 단점들을 개선해야한다는 점은 분명해보입니다. 플러터-다트 언어 개발팀은 이 문제를 다음과 같이 정리하였습니다.
참 적절하고 간략하게 정리하여서 배울 점이 많았습니다.

(1) 조건 - requirements

  1. 현재 존재하는 프로그램에 스며들 수 있어야 한다. Must be able to introspect on the existing program. 
  2. 현재 존재하는 클래스들에 새로운 API로 합성이 가능해야한다.  Must be able to synthesize new APIs in existing classes.
  3. 완전히 새로운 클래스들로 합성가능해야한다.  Should be able to synthesize whole new classes.
  4. 메타프로그래밍으로 구성되어야한다. Metaprogramming should be composable.
  5. 생성된 코드들은 유저들이 볼수 있고 디버깅 할수 있어야한다. Generated code must be debuggable and user visible. 

(2) 디자인 제약

  1. 모듈 컴파일을 지원해야한다 -  Must support modular compilation.
  2. 증분 컴파일을 지원해야한다 - Must support incremental compilation.
  3. 표현식 평가지원을 해야한다 -  Must support expression evaluation.
더보기

 

  • 모듈화된 컴파일 지원 (Modular Compilation Support): 모듈화된 컴파일은 소스 코드를 독립적인 모듈(즉, 컴포넌트)로 나누어 컴파일할 수 있는 기능을 말합니다. 이 기능은 대규모 프로젝트에서 코드의 관리와 빌드를 더 효율적으로 할 수 있도록 도와줍니다. 모듈화된 컴파일을 지원하는 시스템에서는 각 모듈을 독립적으로 컴파일한 후, 필요한 모듈만 다시 컴파일할 수 있어 전체 빌드 시간을 단축할 수 있습니다.
  • 증분 컴파일 지원 (Incremental Compilation Support): 증분 컴파일은 코드의 일부만 변경되었을 때 전체 프로젝트가 아니라 변경된 부분만 컴파일하는 기능을 의미합니다. 이 방식은 전체 프로젝트를 다시 컴파일하는 것보다 빠르게 결과를 얻을 수 있으며, 개발 효율성을 크게 향상시킬 수 있습니다. 증분 컴파일을 지원하는 컴파일러는 이전 컴파일 결과를 저장하고, 변경된 부분만 재컴파일하여 빠른 빌드를 제공합니다.
  • 표현식 평가 지원 (Expression Evaluation Support): 표현식 평가는 코드의 특정 표현식(예: 변수, 수식, 함수 호출 등)의 값을 실행 시간에 평가하고 결과를 확인할 수 있는 기능을 말합니다. 이 기능은 디버깅 시에 특히 유용하며, 실행 중에 코드의 상태를 분석하고 문제를 추적하는 데 도움이 됩니다. 표현식 평가를 지원하는 도구나 환경에서는 코드의 현재 상태를 실시간으로 검사하고, 변수 값이나 계산 결과를 확인할 수 있습니다.

 

(3) 목표가 아닌 것

현재 코드를 수정하는 것이 아니어야한다
 

3. 결과 - macros의 특징

(1) 디자인 설계

  1. 별도의 추가 커맨드 없음 -  Nothing extra to run; 
  2. 중복 작업 없음 - No duplicated work
  3. 데이터에 새로운 파일이 쓰여지지 않음 - Not written to disk,

(2) freezed annotation과 비교

 freezed+json_annotation
macros
import 'package:freezed_annotation/freezed_annotation.dart';

// : associates our `main.dart` with the code generated by Freezed
part 'profile_class.freezed.dart';
// optional: Since our Person class is serializable, we must add this line.
part 'profile_class.g.dart';

/// 프로필 클래스
@freezed
class Profile with _$Profile {
    /// 프로필 초기화
    const factory Profile({
    required String name,
    required String email,
    required String phone,
    @Default('visitor') String userType,
    @Default([]) List<String> snsUrl,
    @Default('') String map,
    @Default('') String portrait,
    @Default('') String desc,
    @Default('') String companyId,
    }) = _Profile;
    
    factory Profile.fromJson(Map<String, Object?> json) =>
        _$ProfileFromJson(json);
}
import 'package:json/json.dart';

/// 프로필 클래스
@JsonCodable()
class Profile {
    final String name;
    final String email;
    final String phone;
    final String userType;
    final List<String> snsUrl;
    final String? map;
    final String? portrait;
    final String? desc;
    final String? companyId;

    static const guest = Profile(
        email: 'guest',
        name: 'guest',
        phone: 'guest',
    );
}

간단하죠? 
사실 제가 가장 마음에 들었던 부분은 별도의 run이 없어서 
물론 이제 막 도입이 시작되고 있기에 copywith도 없고 커스텀에도 불편한 부분들이 많았지만 아주아주아주아주 큰 변화임엔 틀림없는 것 같습니다. 이제 제가 글 도입부에서 호들갑을 떤 이유가 조금 납득이 되셨을까요?

여러분은 어떻게 생각하시나요, macros는 flutter 개발에 얼만큼 영향을 줄까요?
https://www.youtube.com/live/AsF_liobO-c?si=Og0SoFkdBQCfdAKh

 

댓글