ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter 모델 생성 코드는 소스가 아니라 산출물입니다
    AI Agent/flutter 2026. 7. 4. 15:00
    728x90
    반응형

    겹쳐 놓인 거의 똑같은 코드 인쇄물 두 장과 대각선 펜 자국
    손으로 고친 .g.dart가 증발한 날

    서버 응답에 필드 하나가 null로 와서 앱이 죽었습니다.
    급한 김에 저는 *.g.dart 파일을 열어서, fromJson 안에 있는 파싱 줄에 직접 ?? ''를 박아 넣었습니다.
    앱은 살아났고, 저는 만족하며 커밋했습니다.
    딱 다음 빌드까지였습니다.

    flutter pub run build_runner build를 한 번 돌리자, 제가 손으로 넣은 ?? ''가 흔적도 없이 사라졌습니다.
    앱은 다시 죽었습니다.
    저는 잠깐 제 눈을 의심했습니다.
    분명 고쳤는데, 파일을 다시 여니 원래대로 돌아가 있었습니다.

    처음엔 저장이 안 됐나 싶어 다시 열어 확인했습니다.
    아니었습니다.
    커밋 로그에도 제 수정이 분명히 남아 있었습니다.
    그런데도 파일 내용은 옛날 그대로였습니다.
    Git이 아니라 도구가 파일을 덮어쓴 거라는 걸, 그 순간엔 상상도 못 했습니다.
    저는 파일을 "제가 고치는 것"이라고만 생각했지, "누가 다시 써내는 것"이라고는 생각해본 적이 없었으니까요.

    생성 파일은 소스가 아니라 스냅샷입니다

    json_serializable이나 freezed 같은 도구는 제가 쓴 모델 클래스를 읽고, 거기서 파싱 코드를 뽑아 *.g.dart에 써넣습니다.
    그러니까 그 파일은 제가 짜는 소스가 아니라, 도구가 만든 산출물입니다.
    마지막 생성 시점의 사진 한 장인 셈입니다.

    사진을 아무리 예쁘게 포토샵해도, 다음에 셔터를 누르면 원래 피사체대로 다시 찍힙니다.
    제가 *.g.dart를 손으로 고친 건, 딱 그렇게 사진 위에 낙서한 거였습니다.
    build_runner를 다시 돌리는 순간 낙서는 지워지고, 도구는 원본 모델만 보고 새 사진을 찍습니다.
    고쳐야 할 곳은 산출물이 아니라 원본, 그러니까 모델 클래스였습니다.

    @JsonSerializable()
    class UserDto {
      const UserDto({required this.id, this.name = ''}); // 기본값은 여기서 줍니다
      final String id;
    
      @JsonKey(defaultValue: '') // null 방어도 원본 모델에 선언합니다
      final String name;
    
      factory UserDto.fromJson(Map<String, dynamic> json) => _$UserDtoFromJson(json);
      Map<String, dynamic> toJson() => _$UserDtoToJson(this);
    }
    

    @JsonKey로 원본에 방어를 선언하니, build_runner를 다시 돌려도 *.g.dart에 그 처리가 매번 다시 새겨졌습니다.
    산출물에 낙서하는 게 아니라, 산출물을 만드는 규칙을 고친 겁니다.

    진짜 지옥은 머지에서 왔습니다

    이 착각이 혼자 짠 코드였으면 그냥 웃고 넘겼을 겁니다.
    문제는 팀이 같이 쓸 때였습니다.
    *.g.dart를 커밋에 포함해두니, 두 사람이 각자 모델을 조금씩 바꾸고 각자 build_runner를 돌리자, 생성 파일 수백 줄이 서로 다르게 다시 써졌습니다.
    머지하는데 충돌이 파일마다 터졌습니다.
    정작 사람이 쓴 코드는 몇 줄 안 겹치는데, 기계가 뽑은 줄에서 충돌이 산더미였습니다.

    그 충돌을 손으로 풀 이유가 없다는 걸 그때 알았습니다.
    어차피 build_runner 한 번이면 원본에 맞게 다시 생성될 파일인데, 그걸 사람이 줄 맞춰가며 병합하고 있었던 겁니다.
    산출물을 소스처럼 다뤘으니 당연한 고생이었습니다.

    한번은 충돌을 반쯤 잘못 풀어서, 한 사람의 필드 변경이 생성 파일에서 누락된 채 머지된 적도 있었습니다.
    원본 모델은 멀쩡한데 *.g.dart만 어긋난 상태였죠.
    다행히 다음 빌드에서 다시 생성되며 맞춰졌지만, 그 사이에 "왜 필드가 파싱이 안 되지" 하고 또 원인을 엉뚱한 데서 찾았습니다.
    산출물을 신뢰의 대상으로 두면, 이렇게 원본과 산출물이 어긋난 틈에서 시간이 샙니다.

    커밋할 것과 다시 만들 것을 나눕니다

    정리하고 나니 기준은 단순했습니다.
    사람이 쓰는 모델은 커밋하고 리뷰하고, *.g.dart는 필요할 때 다시 만들면 되는 것으로 분리했습니다.
    팀에 따라 생성 파일을 아예 .gitignore에 넣고 CI에서 매번 생성하기도 하고, 커밋하되 "이건 손대지 말 것"이라고 못을 박기도 합니다.
    어느 쪽이든 핵심은 같습니다.
    그건 고칠 파일이 아니라 다시 찍을 사진이라는 것.

    이 기준이 서니 리뷰도 편해졌습니다.
    PR에 *.g.dart 변경이 수백 줄 껴 있어도, 그 줄들을 한 줄씩 읽지 않습니다.
    원본 모델이 바뀌었으면 생성 파일도 당연히 바뀔 뿐이니까요.
    제가 눈여겨볼 건 사람이 쓴 모델 몇 줄이고, 생성 파일은 그 모델과 아귀가 맞는지만 봅니다.
    봐야 할 것과 안 봐도 되는 것이 갈리니, 리뷰에 드는 힘도 그만큼 줄었습니다.

    한 가지 조심할 게 더 있었습니다.
    좁게 다시 만들려고 --delete-conflicting-outputs를 붙였다가, 생각보다 많은 파일이 함께 지워져서 잠깐 놀란 적이 있습니다.
    지워져도 어차피 다시 생성되긴 하는데, 그 순간엔 심장이 철렁합니다.
    다음에 또 *.g.dart에 손이 가려고 하면, 저는 파일을 열기 전에 그게 소스인지 사진인지부터 물어볼 겁니다.

    원문 기준

    원문 기준: https://dart.dev/tools/build_runner

    확인일: 2026-07-01.
    build_runner 플래그는 버전에 따라 동작이 달라질 수 있어서, 올리기 전에 옵션 설명을 다시 봅니다.

    728x90
    반응형
Designed by Tistory.