View
[Flutter] Visibility 위젯의 maintainState 프로퍼티, Offstage 위젯
sm_amoled 2024. 6. 5. 12:34이번에는 아래와 같은 UI를 만들어보고 싶었다. 내가 선택한 텍스트필드에 포커스가 가고, 다음 버튼을 누르면 새로운 필드가 열리며 여기로 포커스가 이동하는 방식을 구현하고 싶었다.
ㅤ
ㅤ
내가 만들고자한 서비스에서는 텍스트필드의 개수가 정해져있었기에, step 으로 나눠 Visibility 위젯으로 이를 숨겨놨었다. 기타 부수적인 코드를 다 제거하면 아래처럼 작성이 되었다.
ㅤ
List<FocusNode> focuseNodeList = [FocusNode(), FocusNode(), FocusNode()];
...
Visibility(
visible: 첫 번째 step 부터,
child: Column(
children: [
Visibility(
visible: viewmodel.focusedStep == 0,
child: Container(
child: const Text(
'도전의 이름을 지어주세요',
),
),
),
TextFormField(
focusNode: focuseNodeList[0],
onChanged: (value) {
setState(() {
provider.setNameString(value);
});
},
),
],
),
),
Visibility(
visible: 두 번째 step 부터,
child: Column(
children: [
Visibility(
visible: viewmodel.focusedStep == 1,
child: Container(
child: const Text(
'달성하려는 목표는 무엇인가요?',
),
),
),
TextFormField(
focusNode: focuseNodeList[1],
onChanged: (value) {
setState(() {
provider.setGoalString(value);
});
},
),
],
),
),
Visibility(
maintainState: true,
visible: 세 번째 step 부터
child: Column(
children: [
Visibility(
visible: viewmodel.focusedStep == 2,
child: Container(
child: const Text(
'어떤 노력을 지속하실 건가요?',
),
),
),
TextFormField(
focusNode: focuseNodeList[2],
onChanged: (value) {
setState(() {
provider.setActionString(value);
});
},
),
],
),
),
ㅤ
여기에서, 다음 버튼을 누르면 step이 넘어가면서 새로운 텍스트필드가 바깥쪽 Visibility 위젯을 통해 visible: true가 되면서 보여지고, 자연스럽게 FocusNode가 해당 필드로 이동이 되도록 코드를 작성하기 위해 아래처럼 코드를 썼다.
WideColoredButton(
onPressed: () async {
// step이
if (viewmodel.currentStep != 4) {
setState(() {
viewmodel.currentStep += 1;
provider.setFocusedStep(viewmodel.currentStep);
provider.checkIsMovableToNextStep();
});
if (viewmodel.currentStep < 3) {
FocusScope.of(context).requestFocus(
focuseNodeList[viewmodel.currentStep],
);
}
...
setState(() {});
}
}
)
ㅤ
그런데, 왠걸? 암만 시도를 해봐도 포커스가 정상적으로 잘 이동하지 않았다. 단순히 FocusNode의 로직을 잘못 작성했나 싶어서 여러 케이스를 테스트했을 때 ‘이미 화면에 나와있는 텍스트필드간의 포커스 전환’은 잘 동작했으나, 유독 Visibility로 숨겨둔 위젯을 꺼내면서 포커스를 전환하는 시점에는 포커스 이동 로직이 원활하게 작동하지 않았다.
ㅤ
문제 해결을 위해 opacity 값을 조절하는 방법을 써야하나…? 를 고민하던 시점에, 구글링을 통해 Visibility 위젯에 maintainState라는 프로퍼티가 있다는걸 알게되었다.
메모리에 State를 담은 Object를 유지하고, 트리 상에서 관리하는 것은 expensive 한 task 이기 때문에, re-create가 불가능한 State 만 유지해야한다. 이를 위해 maintainState 프로퍼티를 사용할 수 있다고 한다.
만약 maintainState를 true로 설정하면, Visibility는 Offstage라는 위젯으로 대체되어 적용된다고 한다 😮😮
ㅤ
Offstage 라는 위젯은 처음 들어봤기에, 당장 링크를 타고 들어가서 확인해봤다.
ㅤ
Offstage 라는 위젯은 이름처럼 뒤에서 작동하는 위젯이다. 사실상 화면에 그려지지 않도록 하는 위젯이라고 보면 될 것 같다. 화면에서 사라질 때 State를 제거해버리는 Visibility 위젯과는 다르게, State 객체와 동작 등을 모두 유지한다고 한다. 이로 인해, 보이지 않는 텍스트 필드가 Focus를 받아 타이핑을 할 수 있게 하고 (이거다!) 실행중인 애니메이션을 가리더라도 이를 계속 실행한다고 한다.
ㅤ
이러한 특성으로, 하위 위젯이 메모리를 계속 점유하고 CPU와 배터리를 계속 잡아먹을 수 있으니 Offstage 위젯은 꼭 필요한 곳에서만 사용해야 한다고 알려준다.
ㅤ
나의 경우에는 Visibility로 숨겨져있던 TextField를 visible 하게 만들어준 다음에 FocusNode로 Focus를 전달하는 로직을 작성했으나, [ visible을 true로 변환 → State Object 생성 ] 을 기다리지 않고 Focus를 변경하는 순서로 코드가 실행되어 의도한대로 전환이 이루이지지 않았다고 생각된다. (그럼 난 로직은 잘 짰던거 아닌가 😢😢)
ㅤ
문제를 고치기 위해서 Visbility 위젯으로 구조를 다 만들어둔 상태이니, 이를 Offstage 위젯으로 변경하지 않고, maintainStage: true 프로퍼티를 추가해 동작을 개선하고자 했다.
아래 코드에서, 안쪽 Visibility의 경우에는 State를 굳이 유지할 필요가 없는 Text 위젯을 담고있기 때문에 이는 maintainState 프로퍼티를 추가해주지 않았다. 바깥쪽 Visibility에 대해서는 TextFormField의 focusNode가 화면에서 보이지 않을 때에도 살아있도록 해줘야 했기 때문에 maintainState 프로퍼티의 값을 true로 지정해주었다.
Visibility(
maintainState: true,
visible: 두 번째 step 부터,
child: Column(
children: [
Visibility(
visible: viewmodel.focusedStep == 1,
child: Container(
child: const Text(
'달성하려는 목표는 무엇인가요?',
),
),
),
TextFormField(
focusNode: focuseNodeList[1],
onChanged: (value) {
setState(() {
provider.setGoalString(value);
});
},
),
],
),
),
ㅤ
수정 후 실행해보니 원활하게 잘 실행이 된다 😄
Visibility 위젯과 Offstage 위젯의 차이를 잘 기억해두자.
번외로, 어제 위 기능을 구현하면서 에뮬레이터에서 실행하는데, FocusNode를 전환할 때 말도 안되게 렉이 걸리는 경우가 발생했다. 거의 2초동안 화면이 정지한채로 아무런 버튼도 눌리지 않는 상태에 빠져버리는 현상이 빈번하게 나타났다. 요 현상은 Focus를 전환하는 코드를 넣을 때만 발생했다! 터미널에는 아무런 에러 로그도 나오지 않았다. 더 이상했던 건 실기기에서는 아주 유려하게 작동했다는 것… 🤔🤔🤔🤔
ㅤ
이게 아무래도 에뮬레이터는 기기 성능이 OS에 의해 제한되어 있기 때문에, 성능이 부족해서 FocusNode를 처리하는데에 렉이 많이 걸리고, 실기기는 성능이 받쳐주기 때문에 괜찮은건가?? 라는 추측을 하고 실기기 빌드에서의 CPU 사용량을 찍어봤다.
ㅤ
ㅤ
그래프 상으로는 실기기에서 두 코드 영역에서 딱히 유의미한 성능 차이가 있는 것 같지는 않았다. 훔,,,?
ㅤ
요 이슈를 이리저리 굴려보다가, 아래 깃허브 글을 발견했다. 나와 똑같은 문제를 겪고 있는 사람들이 역시나 있었어!
https://github.com/flutter/flutter/issues/113192
ㅤ
대충 그 내용을 살펴보자니, iOS의 네이티브 텍스트필드에 커서가 깜빡거리는 특유의 애니메이션이 있는데, 플러터에서 이 커서 애니메이션을 모방하고자 삽입한 애니메이션이 유난히 성능을 많이 잡아먹는 현상이 발견되어 업데이트가 될 예정이라고 언급하고 있다.
ㅤ
웃긴건 지금 해당 문제를 재현해서 GIF를 따 블로그에 올리고 싶었는데, 시뮬레이터와 실기기 모두에서 지금은 원활하게 FocusNode 쪽 코드가 작동하고 있다는 것이다. 아주 매끄럽게.(아 ㅋㅋ)
ㅤ
그때 에뮬레이터를 실행하는 시점에 내 맥 메모리 16GB 중에서 14GB 이상 사용 중 + 가상 메모리 공간도 5GB 넘게 사용중이였던걸 봤을 때, 아마도 실행 환경이 너무 가혹해서 그런 결과가 나온게 아닌가 싶기도 하다. 그래도 다른 기능을 실행할 때는 그렇게 느려지는 현상이 한 번도 발생하지 않았다는걸 고려해볼 때, TextField와 FocusNode의 조합이 꽤나 성능을 많이 사용하는 조합이라는걸 좀 유의해야 할 것 같다.
ㅤㅤ
ㅤ
+)
CPU 성능이 50%를 차지하는게 나는 그러려니 하고 있었는데, 엄청나게 CPU 자원을 빨아먹고 있는 중인가보다?? 위 자료를 서치하려 다니면서 그래프를 봤는데, 50~60% 정도의 CPU 점유율을 보이는 구간에 대해서 “우주에 있는 모든 문제를 해결하는 중인가요?? CPU 사용량이 이 무슨 경우죠??” 라고 글을 작성해뒀다. 50%가 그렇게 비싼 cost인가???? 나는 거의 30~60%가 기본인 것 같던데, 그냥 농담을 던진건가??
ㅤ
ㅤ
사실 내가 개발한 앱의 CPU 사용량, 배터리 impact에 대해선 전혀 신경을 쓰지 않고 있었는데, 위 깃허브 아티클을 보고나서 나도 내가 만든 앱의 서비스를 이용할 때 드는 CPU, Energy Cost를 한 번 확인해봤다.
ㅤ
깜짝 놀라서 플러터 성능에 대해 찾아보니, 깃허브의 어떤 이슈에서는 ‘플러터로 만든 앱을 XCode에서 평가할 때, 배터리 소요 부분이 부정확하게 나온다’ 라고 의견을 주고받고 있기는 하다.
We did some research and find the Xcode/instrument's battery profiling tool to be quite unreliable and inaccurate. In the long run, we might be adding some special hardware (e.g., Monsoon) to our lab to accurately measure the voltage and amps to calculate the exact power usage.
For now, we think that CPU and GPU percentage can be used to estimate Flutter's power usage as screen brightness and wireless signals are not directly managed by Flutter.
ㅤ
그럼에도, 요런 그래프들을 간간이 참고해서 앱 성능에 대한 고려 및 배터리 컨슘 이슈를 최소화를 위해 더 공부하고 신경쓰면서 리펙토링을 나중에 한 번 적용해봐야겠다.
샤라웃
'Develop > Flutter 개발' 카테고리의 다른 글
[Flutter] Image.file 은 File 데이터의 변경사항을 반영해주지 않는다. 대신 캐싱 관리가 쉬운 FileImage를 사용하자! (0) | 2024.06.19 |
---|---|
[Flutter] Apple 로그인 창 Modal 안뜨는 경우 / AuthorizationErrorCode.unknown error 1000. (0) | 2024.06.14 |
[Flutter] 자식 위젯에서 부모 위젯 setState 호출하기 (0) | 2024.05.23 |
[Flutter] Firebase에서 닉네임 문자열 필드에 대한 검색(인 척 하는) Query 기능 구현 (0) | 2024.05.22 |
[Flutter] map 함수는 단순히 리스트를 순회하는 함수가 아니다 (0) | 2024.05.18 |