발행일

일정 데이터 동기화 문제 해결하기 — 그리고 이 해결책은 나중에 뒤집혔다

일정 데이터 동기화 문제 해결하기

[TripTune](/blog/웹 기술로 만드는 협업형 여행 계획 플랫폼 TripTune 개발) 일정 편집 화면의 여행 루트 목록에서 동기화 문제가 반복되던 시기의 기록입니다. 이 글은 그 싸움의 중간 기록이고, 결말은 당시 예상과 달랐습니다 — 끝에 붙였습니다.

날짜 정정 — 이 글은 원래 2023-09-10으로 게시돼 있었는데, 프로젝트 시작(2024-06)보다 앞선 불가능한 날짜였습니다. 해당 로직(fetchAndMergeRoutes)이 들어간 커밋 기준으로 2025-01-11로 교정합니다.

1. 무엇이 반복해서 깨졌나

  • 과거 데이터 병합 오류 — 새 페이지 데이터만 추가하다 보니 이미 화면에 있는 항목과 어긋남
  • 삭제 미반영 — 사용자가 루트에서 뺀 장소(deletedPlaces)가 다음 페이지 로드 때 되살아남
  • 중복/누락 — react-query 무한 스크롤의 캐시와 편집 중인 목록이 서로 다른 버전을 들고 있음

2. 당시의 해결 — 병합 로직 재작성

스토어 액션에서 전 페이지를 순회해 가져온 뒤, "이미 있는 항목"과 "삭제된 항목"을 걸러 병합하도록 재작성했습니다.

fetchAndMergeRoutes: async (scheduleId: number) => {
  let currentPage = 1;
  let totalPages = 1;
  let allRoutes: Place[] = [];

  while (currentPage <= totalPages) {
    const response = await fetchTravelRoute(scheduleId, currentPage);
    if (!response.success) break;
    totalPages = response.data.totalPages;
    allRoutes = [...allRoutes, ...response.data.content];
    currentPage++;
  }

  set((state) => ({
    travelRoute: [
      ...allRoutes.filter(
        (newRoute) =>
          !state.travelRoute.some((r) => r.placeId === newRoute.placeId) &&
          !state.deletedPlaces.includes(newRoute.placeId),
      ),
      ...state.travelRoute,
    ],
  }));
};

삭제 반영과 중복 제거는 이 필터로 실제로 잡혔고, 당시 글의 결론은 "React Query와 명시적 병합 로직으로 해결"이었습니다.

3. 그 후 — 이 해결책은 뒤집혔다

몇 달 뒤, 이 화면에서 react-query를 아예 걷어냈습니다. 병합 로직을 아무리 다듬어도 근본 구조가 문제였거든요 — 같은 목록을 react-query 캐시와 zustand 스토어가 둘 다 소유하는 한, 병합은 계속 필요하고 병합이 있는 한 충돌 케이스는 계속 나옵니다. 편집 가능한 목록의 주인은 하나(스토어)여야 한다는 게 최종 결론이었고, 그 관점 정리는 무한 스크롤에서는 왜 이슈가 생긴 걸까에 있습니다.

그래서 이 글의 가치는 "해결책"이 아니라 중간 기록입니다. 증상(중복·누락·부활)을 필터로 막는 단계를 거쳐야, "필터가 계속 필요하다는 것 자체가 설계 신호"라는 걸 볼 수 있었으니까요.

4. 다시 보니 — 이 코드 자체의 문제들

결말과 별개로, 위 코드에는 지금 보면 걸리는 게 셋 있습니다.

  • "무한 스크롤"이 아니라 전량 재조회입니다. while (currentPage <= totalPages) — 호출될 때마다 1페이지부터 끝 페이지까지 전부 다시 불러옵니다. 스크롤 바닥에 닿을 때마다 전체 목록을 재다운로드하는 구조라, 페이지가 늘수록 비용이 선형으로 커져요. 병합 오류를 "매번 다 새로 가져오기"로 덮은 셈인데, 당시엔 그게 은폐라는 걸 몰랐습니다.
  • 병합 순서가 이상합니다. 새로 걸러낸 항목을 기존 목록 앞에 붙입니다([...filtered, ...state.travelRoute]). 서버 순서 기준이면 새 페이지는 뒤에 와야 하고, 사용자 편집 순서 기준이면 애초에 서버 순서를 덮으면 안 되죠. 이 애매함 자체가 "주인이 둘"이라는 문제의 증상이었습니다.
  • 실패가 조용합니다. 중간 페이지가 실패하면 break로 부분 데이터만 병합하고 사용자에게는 아무 표시가 없습니다. 부분 성공은 침묵할 게 아니라 알려야 하는 상태였어요.

정리

  • 증상 필터(중복 제거·삭제 반영)가 계속 필요하다면, 그건 유지할 로직이 아니라 설계를 의심하라는 신호였습니다.
  • "해결했다"고 쓴 글도 시간이 지나면 중간 기록이 됩니다. 결말을 이어 붙일 수 있다는 게 기록을 남기는 이유고요.