[개발하는남자 핸즈온 플러터] 챕터11 GetxListener 문제 확인 및 리펙토링

[개발하는남자 핸즈온 플러터] 챕터11 GetxListener 문제 확인 및 리펙토링

조회수 20

개요

안녕하세요 개발하는남자 개남입니다. 지난 포스트에 이어서 핸즈온 플러터 챕터 11장을 리뷰 하겠습니다. 이번장에서는 최신화를 진행할 것은 없고 대신 소스코드에 다소 문제가 확인되어 어떤문제인지를 공유하고 정정하는 리뷰를 진행하도록 하겠습니다.

소스코드 : https://github.com/sudar-life/bamtol_market_clone_coding
브런치 정보 : chapter11
플러터 버전 : Flutter 3.32.5
Dart 버전 : Dart 3.8.1

문제확인

책에 기록된 내용은 그대로 이해하시고 진행하셔도 앱을 개발하는데에는 문제가 되지 않습니다. 하지만 리뷰하는 중에 의문이 드는 부분이 발생되어 해당 내용을 디버깅 및 리서치를 해본 결과를 공유드립니다.

의문이 들었던 부분

@override Widget build(BuildContext context) { return Scaffold( body: Center( child: GetxListener<bool>( listen: (bool isLogined) { if (isLogined) { Get.offNamed('/home'); } else { Get.offNamed('/login'); } }, stream: Get.find<AuthenticationController>().isLogined, child: GetxListener<bool>( //이하 생략

GetxListener 위젯에서 AuthenticationControllerisLogined 의 상태 변화를 stream 구독하고 있어 변경사항에 대해서 isLogined일때는 home화면으로 아닌경우는 login화면으로 Get.offNamed 처리 하고 있습니다.

이동된 home위젯에서 임의로 글씨에 터치 이벤트를 걸어 AuthenticationControllerisLoginedfalse 시킴으로 로그아웃 상태를 만들고 login 페이지로 redirect되는 것을 설명했습니다.

그리고 테스트 진행해봐도 정상적으로 예상했던 대로 isLoginedfalse가 되면 login 페이지로 랜딩되는 것을 확인할 수 있습니다.

여기서 위젯트리를 이해하고 route를 이해하시는분이라면 의문이 들수 있습니다. 저 역시 책을 집필당시에는 알아차리지 못했지만 리뷰를 하면서 소스코드를 보니 의문이 들어 몇가지를 검토해보니 문제되는 부분이 확인되었습니다.

우선 Get.offNamed의 성질을 이해해야 합니다. Get.offNamed는 현재 패이지를 off(종료)해버리고 새로운 페이지로 랜딩시키는 성질이 있습니다. 그럼 지금 페이지인 SplashPage페이지가 off되며 새로운 페이지 home이나 login페이지로 대체된다는 것인데 redirect 시켜주는 로직은 SplashPage에 있어서 HomePage에서 로그아웃을 한다고 해도 그것을 처리하던 페이지인 SplashPage가 종료되었으니 redirect가 되지 않는것이 맞습니다.

이혜를 돕기 위해 다이어그램을 보시면 이해가 되실 것입니다. process.png

하지만 프로그램을 돌려 테스트 해보면 정상적으로 종료된 SplashPage에서 디버깅도 걸리고 원하는 대로 요청이 처리 되는 것을 확인할 수 있는데요 그렇다면 이것은 버그일까요?

원하던 대로 작동이 되더라도 의도하지 않는것이기 때문에 버르가 볼 수 있습니다. 그렇다면 왜 이미 종료된 Page임에도 불구하고 steam 동작은 되는 걸까요? 그 이유는 stream을 종료 하지 않았기 때문입니다. steam은 선언이 되면 전역으로 사용되는 것과 같은 효과가 있습니다. 고로 로직이 있던 Page가 종료 되더라도 steam은 종료 되지 않았기 때문에 작동되던 것입니다.
전역 변수에 대해서 지난 포스팅에서 다룰때 단점으로 꼽은 것중에 메모리 누수가 있습니다. 접근은 편하고 좋지만 전역에 선언한 것은 앱이 종료되기까지 없어지지 않기에 메모리 관리를 잘해주지 않으면 누수가 될 수 있습니다. stream도 마찬가지 입니다. stream 등록을 했던 페이지가 종료된다면 의도상 사용중이던 stream 역시 종료 시켜야 맞습니다. 그렇지 않으면 알수 없는 페이지에서 지속적인 메모리 사용을 하더라도 어디서 사용되는지 찾기가 어려워져 나중에 관리가 힘들어질 수 있습니다.

소스개선

문제를 확인했으니 이제 개선을 통해 대응을 해보도록 하겠습니다.

GetxListener 소스 리펙토링

1.initState로 stream Subscription 등록

기존에 listener를 등록하는 곳을 createState 함수를 이용해서 처리했는데 종료처리 관리를 위해 state 클래스 내부에 StreamSubscription 객체로 등록해서 dispose 할때 이를 반환하도록 처리 하겠습니다.

StreamSubscription<T>? _subscription; void initState() { super.initState(); // 위젯 생명주기에 맞춰 리스너 등록 _subscription = widget.stream.stream.listen(widget.listen); if (widget.initCall != null) { widget.initCall!(); } }

2.dispose 종료시 반환처리

GetxListener 위젯에 dispose 함수를 추가 하여 등록된 stream을 반환(종료)처리 하겠습니다.

💡 참고
dispose 함수는 StatefulWidget의 라이프사이클에서 위젯이 종료되면 호출되는 함수입니다.

void dispose() { // 위젯 소멸 시 리스너 해제 print('dispose : ' + widget.stream.value.toString()); _subscription?.cancel(); _subscription = null; super.dispose(); }

print로 어떤 stream 값인지 확인하고 반환 되는지 확인하기 위해 디버깅용 로그를 남기고 initState때 등록해줬던 _subscription 객체변수를 활용해서 cancel 함수를 호출하면 stream을 종료 시킬 수 있습니다.

SplashPage 위젯 소스 리펙토링

이곳에서 수정해줘야 하는 부분은 SplashPage는 종료가 될 페이지이기 때문에 종료 되도 괜찮은 stream만 이곳에서 관리하고 auth 인증 관련 stream을 상위 위젯으로 빼주는 작업을 해줘야 합니다. 하지만 수정하기 전 이상태로 앱을 구동했을때 정상적으로 로그아웃시 패이지가 로그인 페이지로 랜딩되지 않아야 정상이므로 이를 확인하고 리펙토링 하도록 하겠습니다.

home_screen.png

예상대로 홈을 터치해도 이전과 달리 라우팅 되지 않는 것을 확인 하실 수 있습니다. 또한 HomePage로 랜딩되면서 Debug Console 로그에는 다음과 같은 로그를 확인할 수 있습니다.

flutter: dispose : StepType.authCheck flutter: dispose : true // 두번 출력

구독중인 stream의 최긴 값을 출력했기 때문에 auth 관련 구독을 제거했고 true값의 대한 구독이 2번 해제된 것 을 확인할 수 있습니다.

이제 의도한대로 작동을 하고 있으니 홈 글씨를 터치하면 이전처럼 로그아웃 처리 되도록 만들어보도록 하겠습니다.

Widget build(BuildContext context) { return Scaffold( body: Center( child: GetxListener<bool>( listen: (bool value) { if (value) { controller.loadStep(StepType.authCheck); } }, stream: Get.find<DataLoadController>().isDataLoad, child: GetxListener<StepType>( initCall: () { controller.loadStep(StepType.dataLoad); }, listen: (StepType value) { switch (value) { case StepType.init: case StepType.dataLoad: Get.find<DataLoadController>().loadData(); break; case StepType.authCheck: Get.find<AuthenticationController>().authCheck(); break; } }, stream: controller.loadStep, child: const _SplashView(), ), ), ), ); }

소스코드에서 GetxListener가 하나가 없어진 것을 확인할 수 있습니다. AuthenticationControllerisLogined를 구독하던 GetxListener를 들어냈습니다. 대신 SplashPage가 종료 되더라도 살아있는 main.dart 파일의 MyApp 위젯에 추가를 해주겠습니다.

Widget build(BuildContext context) { return GetMaterialApp( title: '당근마켓 클론코딩', initialRoute: '/', theme: ThemeData( appBarTheme: const AppBarTheme( elevation: 0, color: Color(0xff212123), titleTextStyle: TextStyle( color: Colors.white, ), ), scaffoldBackgroundColor: const Color(0xff212123), ), initialBinding: BindingsBuilder(() { Get.put(SplashController()); Get.put(DataLoadController()); Get.put(AuthenticationController()); }), getPages: [ GetPage(name: '/', page: () => const App()), GetPage(name: '/home', page: () => const HomePage()), GetPage(name: '/login', page: () => const LoginPage()), ], builder: (context, child) { // ------------- 추가 return GetxListener<bool>( listen: (bool value) { if (value) { Get.offNamed('/home'); } else { Get.offNamed('/login'); } }, stream: Get.find<AuthenticationController>().isLogined, child: child ?? const SizedBox.shrink(), ); }, ); }

GetMaterialApp 위젯의 옵션중 builder라는 옵션이 있으며 이는 기존 Widget에 있는 옵션입니다. 모든 페이지는 builder를 거쳐서 그려지는 것으로 가장 최상위의 위젯을 추가 할 수 있습니다.

이 부분에 기존 위젯을 GetxListenerwrapping 하여 처리 하도록 수정했습니다.

이제 소스코들 실행하면 정상적으로 터치시 로그인 페이지로 랜딩되는 것을 확인할 수 있습니다.

#개발하는남자 핸즈온 플러터#책리뷰#챕터11#GetxListener#당근마켓 클론코딩#flutter

Write by :

개발하는남자

개발하는남자

Developer

💬댓글 0

💬댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...