API 호출 최적화에 대한 고민
기존에는 탭에 들어갈 때마다 `initState()`에서 api를 호출해서 데이터를 가져오는 구조였다.
그래서 탭에 들어갈 때마다 로딩스피너가 돌아가다가 데이터가 뜨는 것을 볼 수 있다.
그런데 교정일기 앱은 사용자가 데이터를 추가하는 경우가 아니면 데이터를 업데이트할 필요가 없기 때문에
불필요한 api 호출을 줄이기 위해 리팩토링을 진행했다.
일단 `AutomaticKeepAliveClientMixin`을 적용해서 탭을 이동해도 상태가 그대로 유지되도록 했다.
최초 1회만 데이터를 받으면 탭을 이동해도 상태가 그대로 유지되기 때문에
무한대로 api를 호출하던 횟수를 홈 탭 2회 + 타임라인 탭 1회 + 앨범 탭 1회, 총 4회로 줄일 수 있었다.
[Flutter] 탭 이동해도 상태 유지하기 | AutomaticKeepAliveClientMixin & PageView
그럼에도 아직 최적화할 수 있는 부분이 남아있다고 생각했다. 타임라인 데이터로 예를 들어 설명하겠다.
- 홈 탭과 타임라인 탭에서는 동일한 데이터를 사용하는데 홈 탭에서는 데이터를 3개만 쿼리하고, 타임라인 탭에서는 전체 데이터를 각각 호출하는 구조였다.
- 사용자가 데이터를 추가하는 경우에 스택이 초기화되면서 모든 탭에서 api를 다시 호출하게 된다.
만약에 데이터를 전역 상태로 관리한다면 다음과 같이 개선될 것이라고 생각했다.
- 타임라인 데이터를 최초 1회만 불러와서 전역 상태에 저장하고, 홈 탭에서는 `timelineData.take(3)`를 보여주고 타임라인 탭에서는 전체 데이터를 그대로 보여준다.
- 만약 사용자가 타임라인 데이터를 추가한다면 타임라인 api만 호출해서 데이터를 업데이트한다.
GetX 컨트롤러 생성하기
import 'package:get/get.dart';
enum TimelineStatus { loading, success, empty, error }
class TimelineController extends GetxController {
var timelineData = <Timeline>[].obs;
var status = TimelineStatus.loading.obs;
@override
void onInit() {
super.onInit();
fetchTimelineData();
}
// Fetch timeline data from provider
void fetchTimelineData() async {
status(TimelineStatus.loading);
try {
var timelines = await TimelinesProvider.fetchTimelineList();
if (timelines.isNotEmpty) {
timelineData.assignAll(timelines);
status(TimelineStatus.success);
} else {
status(TimelineStatus.empty);
}
} catch (e) {
status(TimelineStatus.error);
}
}
// Refresh timeline data
void refreshTimelineData() {
fetchTimelineData();
}
// Method to reset the controller's data (e.g., after logout)
void resetTimelineData() {
timelineData.clear();
status(TimelineStatus.loading);
}
}
- 전역 상태 관리를 위해 컨트롤러 클래스를 만들었다.
- 상태를 `obs`로 선언하면 상태가 변경되었을 때 이를 감지해서 자동으로 UI를 업데이트할 수 있다.
- 전역 상태로 데이터를 관리하면 `FutureBuilder`를 사용할 수 없기 때문에 데이터 상태에 따라 다른 UI를 제공하기 위해 `TimelineStatus`라는 `enum`을 생성했다.
final TimelineController timelineController = Get.put(TimelineController());
- `BottomNavigationBar`를 관리하는 스크린 위젯에서 컨트롤러를 초기화한다.
위젯에 전역 상태 적용하기
final TimelineController timelineController = Get.find<TimelineController>();
...
Expanded(
child: Obx(() {
switch (timelineController.status.value) {
case TimelineStatus.loading:
return const LoadingSpinner();
case TimelineStatus.success:
return _buildTimelineList(timelineController.timelineData);
case TimelineStatus.empty:
return Center(
child: Text(
Messages.noDataCreated('타임라인'),
),
);
case TimelineStatus.error:
return ErrorScreen(
refreshPage: timelineController.refreshTimelineData,
);
default:
return const SizedBox.shrink();
}
}),
),
- 타임라인 탭에서 `TimelineStatus`의 상태에 따라 사용자에게 각기 다른 UI를 제공한다.
- 사용자가 타임라인 데이터를 추가하는 경우에는 `Get.find<TimelineController>().refreshTimelineData();`를 실행해서 데이터를 업데이트한다.
전역 상태 적용한 결과
앱이 시작되면 홈 탭을 빌드하기도 전에 `TimelineController`가 초기회되면서
`onInit()`에 `fetchTimelineData()`를 실행하기 때문에 아예 로딩스피너가 사라진 것을 확인할 수 있다.
덕분에 api 호출도 최적화하고 사용자들에게 더 빠르고 쾌적한 사용환경을 제공할 수 있게 되었다.
결과적으로 무한대로 api를 호출하던 횟수를 타임라인 데이터 1회 + 앨범 데이터 1회, 총 2회로 줄일 수 있었다.
또한 데이터를 추가할 경우 모든 api를 다시 호출하지 않고 변경된 데이터의 api만 최소한으로 호출할 수 있게 되었다.