오늘은 내가 그간 제대로 몰랐던 부분을 간단히 정리해 보려고 한다.
flutter로 리팩토링을 하다 보면 위젯을 함수와 클래스로 분리할 때 차이점이 궁금해지는데
그 당시 읽은 게 전설이 된 레미가 하드캐리한 질문하고 셀프 답변한 글이다.
What is the difference between functions and classes to create reusable widgets?
난 이걸 대충 읽은 바람에 항상 헷갈리는 게 있어 오늘 다시 읽어보게 됐다.
Widget shape() {
return condition ? const Square() : const Circle();
}
"이렇게는 써도 되나? ㅇ..아마도...?😢"
...
일단 레미와 플러터 팀이 class를 더 선호하는 이유를 간단히 알아보자.
✅ 성능 최적화
위와 같이 메서드로 분리한 위젯에서 setState
를 호출하는 경우, 실제론 해당 클래스(BigUIElement)에서 호출한 거니까 당연히 전체 위젯이 빌드된다.
✅ 더 간편한 테스트
저 위에 좋아요 버튼을 예를 든다면, 테스트 코드 작성 시 단일 클래스로 분리한 경우 해당 위젯만 테스트하기 편하다.
하지만 메서드로 분리한 경우 해당 위젯에는 또 다른 위젯들과 요소들이 산재해있으므로 좋아요 버튼 테스트에 필요하지 않은 값들까지 세팅해야 한다.
✅ 정확성
예를 들어, Builder
사용 등으로 context가 여러 개라 ctx, innterContext처럼 다른 명칭을 사용하는 경우 코드가 낡기 쉽다. 이를 class로 분리하면 버그를 줄일 수 있다.
레미의 예제 3개 중 2개는 위젯을 함수로 분리했을 때 context를 잘못 사용해서 불필요한 리빌드나 버그가 발생하는 상황인 만큼 정확한 컨텍스트는 중요하다.
등등이 있다.
여기까진 플러터버라면 이미 알고 있는 내용일 것이다.
그럼에도 나는 늘 마음 한 편이 찝찝했는데 상태 관리와 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
(seeWidget.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.
플러터 시작한지 일주일 된 왕초보입니다. ㅎㅎ 무슨 말인지 절반 이상은 이해못했지만.. ㅋㅋ 그래도 가볍게 정리하는대는 큰 도움이 되었습니다. ㅎㅎ 감사합니다!!