Flutter 위젯 분리 시 class vs function

viiviii·2022년 9월 18일
2
post-thumbnail

오늘은 내가 그간 제대로 몰랐던 부분을 간단히 정리해 보려고 한다.

flutter로 리팩토링을 하다 보면 위젯을 함수와 클래스로 분리할 때 차이점이 궁금해지는데

그 당시 읽은 게 전설이 된 레미가 하드캐리한 질문하고 셀프 답변한 글이다.

What is the difference between functions and classes to create reusable widgets?

난 이걸 대충 읽은 바람에 항상 헷갈리는 게 있어 오늘 다시 읽어보게 됐다.

Widget shape() {
    return condition ? const Square() : const Circle();
}

"이렇게는 써도 되나? ㅇ..아마도...?😢"

...

Class 방식의 장점

일단 레미와 플러터 팀이 class를 더 선호하는 이유를 간단히 알아보자.

Performance

✅ 성능 최적화

  • class는 const 생성자를 사용할 수 있음
  • 불필요한 rebuild 방지

위와 같이 메서드로 분리한 위젯에서 setState를 호출하는 경우, 실제론 해당 클래스(BigUIElement)에서 호출한 거니까 당연히 전체 위젯이 빌드된다.

  • 보라색 영역만 빌드하고 싶었지만 초록색 영역까지 불필요하게 빌드 됨

Testability

✅ 더 간편한 테스트

저 위에 좋아요 버튼을 예를 든다면, 테스트 코드 작성 시 단일 클래스로 분리한 경우 해당 위젯만 테스트하기 편하다.

하지만 메서드로 분리한 경우 해당 위젯에는 또 다른 위젯들과 요소들이 산재해있으므로 좋아요 버튼 테스트에 필요하지 않은 값들까지 세팅해야 한다.

Accuracy

✅ 정확성

예를 들어, Builder 사용 등으로 context가 여러 개라 ctx, innterContext처럼 다른 명칭을 사용하는 경우 코드가 낡기 쉽다. 이를 class로 분리하면 버그를 줄일 수 있다.

레미의 예제 3개 중 2개는 위젯을 함수로 분리했을 때 context를 잘못 사용해서 불필요한 리빌드나 버그가 발생하는 상황인 만큼 정확한 컨텍스트는 중요하다.

이 밖에

  • devtool에서 의미있는 이름으로 위젯을 확인 가능함
  • 더 나은 오류 메세지
  • key 정의 가능

등등이 있다.

내가 모르는 것

여기까진 플러터버라면 이미 알고 있는 내용일 것이다.

그럼에도 나는 늘 마음 한 편이 찝찝했는데 상태 관리와 context가 필요하지 않은 진짜 작은 위젯은 function 방식을 써도 되는 것 아닌가?라는 의문이 있었고 실제로 그렇게 사용하기도 했다. 마음이 개운하지 않은 채!

나는 무얼 모를까?

There is an important difference between using functions instead of classes, that is: The framework is unaware of functions, but can see classes.

난 이 부분을 이해하지 못했다🫠 프레임워크가 function은 알지 못하지만 class는 볼 수 있다니!

도대체 알 수 없고 볼 수 있다는 말이 뭘까? 그저 코드일 뿐이고 렌더 트리를 잘만 그리는데 왜 인식할 수 없다는 걸까

내가 이번에 이해한 것

'내'가 알 수 있다

✅ class 방식인 경우 devtool의 위젯 트리에서 클래스명이 보이므로 '내'가 이해하기 쉽다

We see ClassWidget in the widget-tree showed by the devtool, which helps understanding what is on screen

내가 잘못 알았던 부분은 function은 알 수 없다는 구절을 function 방식으로 생성한 위젯은 devtool의 위젯 트리에서 알 수 없다고 오역한 것이다. devtool의 위젯 트리는 잘만 보여준다.

저 말은 function 방식인 경우, 메서드 명을 square라고 의미 있게 지었어도 devtool Inspacter에서는 해당 이름을 볼 수 없다는 것이다.

클래스 방식인 경우, 하위 트리가 깊어도 이 하위는 Square, 네모를 그린다는 걸 알 수 있다.

그리고 조건 ? circle() : square() 같은 로직이 있으면 함수 방식은 현재 그려진 도형이 circle인지 square인지 더 알아보기 어려워진다.

'프레임워크'가 알 수 있다

그럼 프레임워크가 알 수 있다는 건 뭘까?

레미의 예제 중 첫 번째 링크를 돌려보면 function은 애니메이션이 동작하지 않는다.

왜?

위젯트리를 봐도 AnimatedSwitcher와 Container 모두 잘 있는데 왜?

그 이유는 AnimatedSwitcher는 child가 새로운 위젯인 경우 fade를 주며 전환하는데

The child is considered to be "new" if it has a different type or Key (see Widget.canUpdate ).

child가 새로운 위젯인지 확인할 때 canUpdate가 false인지 확인하는데 이 로직은 runtime type이 같은지를 비교하기 때문이다.

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
}

그래서 key가 없을 때 클래스로 분리하지 않으면 (함수로 분리 or 분리하지 않음)
프레임워크는 old, new의 runtimeType이 모두 Container이므로 새로운 위젯이 아니라고 판단하는 것이다.

클래스로 분리하면 프레임워크는 Square 위젯이 Circle 위젯으로 바뀐 것을 잘 알 수 있게 된다.

이런 종류는 사실 개발하면서 예측하기 어려운데 클래스로 분리하면 이런 실수를 미연에 방지할 수 있다.

Widget shape() {
    return showCircle ? const Circle() : const Square();
}

이제 처음에 고민했던 위 예시처럼 사용해도 된다는 것을 알게 됐다.

메서드로 위젯을 리턴하는 코드 자체가 나쁘다는 것이 아니라 클래스 추출, 분리를 클래스로 하는 것이 좋다라는 것 같다

레미도 이 부분을 확실히 했다.

EDIT: To make up for some misunderstanding: This is not about functions causing problems, but classes solving some.


참고 링크

2개의 댓글

comment-user-thumbnail
2023년 6월 11일

플러터 시작한지 일주일 된 왕초보입니다. ㅎㅎ 무슨 말인지 절반 이상은 이해못했지만.. ㅋㅋ 그래도 가볍게 정리하는대는 큰 도움이 되었습니다. ㅎㅎ 감사합니다!!

1개의 답글