티스토리 뷰

 

Flutter에서 일정 추가 기능을 구현할 때 서버와의 통신 시간이 걸리더라도 화면이 끊기지 않게 하려면 어떻게 해야 할까?

오늘은 "낙관적 업데이트(Optimistic Update)" 패턴을 적용한 코드를 살펴보자.

문제 상황

Schedule을 추가할 수 있는 간단한 UI를 구현한 다음, 여기에 3월 15일 오후 2시 회의 일정을 추가한다고 해보자.

서버에 저장 요청을 보낸 뒤 응답을 기다리는 동안 아무 반응이 없다면 사용자 경험이 좋다고 볼 수 없으니..!

여기서 낙관적 업데이트 패턴을 적용하면 이런 문제를 해결할 수 있다.

화면 예시

코드 예시

class ScheduleProvider extends ChangeNotifier {
  final ScheduleRepository repository;

  DateTime selectedDate = DateTime.now().toUtc();
  Map<DateTime, List> cache = {};

  ScheduleProvider({required this.repository}) {
    getSchedules(date: selectedDate);
  }

  void createSchedule({required ScheduleModel schedule}) async {
    final targetDate = schedule.date;

    const uuid = Uuid();
    final tempId = uuid.v4();
    final newSchedule = schedule.copyWith(id: tempId);

    cache.update(
      targetDate,
      (value) => [...value, newSchedule]..sort((a, b) => a.startTime.compareTo(b.startTime)),
      ifAbsent: () => [newSchedule],
    );

    notifyListeners();

    try {
      final savedSchedule = await repository.createSchedule(schedule: schedule);

      cache.update(
        targetDate,
        (value) => value
          .map((e) => e.id == tempId ? e.copyWith(id: savedSchedule) : e)
          .toList(),
      );
    } catch (e) {
      cache.update(
        targetDate,
        (value) => value.where((e) => e.id != tempId).toList(),
      );
    }

    notifyListeners();
  }
}
  

코드 동작 흐름

1. 임시 ID 생성

사용자가 일정을 추가하려고 하면 먼저 Uuid 라이브러리를 사용해서 임시 ID를 만든다.

예) "temp-123-456" 

const uuid = Uuid();
final tempId = uuid.v4();

2. 즉시 UI 업데이트

생성된 임시 ID와 함께 캐시를 업데이트한다.

서버 응답을 기다리지 않고 화면에 새 일정이 바로 표시된다.

cache.update(
  targetDate,
  (value) => [...value, newSchedule]..sort((a, b) => a.startTime.compareTo(b.startTime)),
  ifAbsent: () => [newSchedule],
);
notifyListeners();
  

예) 3월 15일 캐시에 추가되는 데이터

  • ID: temp-123-456
  • 내용: "회의"
  • 시간: "14:00"

3. 서버 저장

이제 서버에 요청을 보내서 실제 일정을 저장하면 서버는 새로 생성된 ID를 반환해준다.

예) "server-789"라는 ID를 돌려줌

4. 성공 시 ID 교체

서버에서 받은 ID로 캐시에 저장된 임시 ID를 교체한다.

화면에는 전혀 변화가 없기 때문에 사용자는 이 과정이 진행된 걸 모른다.

cache.update(
  targetDate,
  (value) => value.map((e) => e.id == tempId ? e.copyWith(id: savedSchedule) : e).toList(),
);
  

5. 실패 시 롤백

만약 서버 저장이 실패했다면 캐시에서 해당 일정을 삭제한다.

추가된 일정이 사라지는 걸 보이기 전에 적절히 오류 화면을 처리해주면 된다.

cache.update(
  targetDate,
  (value) => value.where((e) => e.id != tempId).toList(),
);
  

결론

낙관적 업데이트 패턴으로 만들면 서버 응답을 기다리는 동안에도 사용자 경험이 매끄럽게 유지되도록 도와준다.