Zustand

Zustand 캐시 문제로인한 데이터 로드 이슈

해보구 2025. 3. 22. 13:10
반응형
최근 POS 시스템을 개발하면서 Zustand를 상태 관리 라이브러리로 사용했다. 이 과정에서 특정 상황에서 메뉴 데이터가 제대로 로드되지 않는 문제를 발견했다. 특히, 스토어 전환 후 currentMenus가 빈 배열로 남아 있거나, 결제 후 /pos 페이지로 돌아올 때 카테고리와 메뉴가 사라지는 현상이 발생했다. 이 문제를 해결하기 위해 캐시 로직을 점검하고 수정했다. 이번 글에서는 그 과정을 정리하고, Zustand 캐시 문제를 해결한 방법을 공유하고자 한다.
 

문제 상황

POS 시스템에서 사용자는 로그인 시 storeId를 선택하고, 해당 스토어의 카테고리와 메뉴를 로드한다. Zustand 스토어(usePosStore)에서 fetchMenusByCategory 함수를 통해 메뉴 데이터를 가져오고, 이를 menuCache에 저장해 캐싱했다. 캐시 로직은 다음과 같았다:

fetchMenusByCategory: async (categoryId: number, forceReload: boolean = false) => {
  const { storeId, menuCache } = get();
  if (!storeId) return;

  set({ isLoading: true });
  const storeCache = menuCache[storeId] || {};
  if (!forceReload && storeCache[categoryId]) {
    console.log(`Using cached menus for storeId: ${storeId}, categoryId: ${categoryId}`);
    set({ currentMenus: storeCache[categoryId], isLoading: false });
    return;
  }
  try {
    const { data } = await axiosInstance.get(`/api/menus/all/${categoryId}`);
    const transformed = data.map((menu: any) => ({
      ...menu,
      menuId: menu.menuId ?? menu.id ?? null,
    }));
    set((state) => ({
      menuCache: {
        ...state.menuCache,
        [storeId]: {
          ...(state.menuCache[storeId] || {}),
          [categoryId]: transformed,
        },
      },
      currentMenus: transformed,
      isLoading: false,
    }));
  } catch (err) {
    console.error(`fetchMenusByCategory error:`, err);
    set({ currentMenus: [], isLoading: false });
  }
},

문제는 다음과 같은 상황에서 발생했다:

  1. 스토어 전환 시: storeId: 3에서 storeId: 4로 전환했을 때, fetchMenusByCategory가 호출되었지만 currentMenus가 빈 배열로 남았다. 로그를 보니 API 호출은 성공했는데도 상태가 업데이트되지 않았다.
  2. 결제 후 복귀 시: 결제 페이지에서 resetData()를 호출해 상태를 초기화한 후 /pos로 이동했는데, categoriescurrentMenus가 빈 상태로 표시되었다.

로그를 분석하며 캐시 로직에 문제가 있음을 의심했다.

 

문제 원인 파악

로그를 꼼꼼히 살펴보며 원인을 추적했다. 주요 원인은 다음과 같았다:

  1. 캐시 우선순위 문제:
    fetchMenusByCategory에서 forceReloadtrue임에도 불구하고, 캐시된 데이터가 우선적으로 반환되었다. 알고 보니, 캐시 체크 로직(if (!forceReload && storeCache[categoryId]))이 의도대로 작동하지 않고, 이전 스토어의 캐시가 새 스토어에 영향을 미쳤다.
  2. API 호출의 storeId 누락:
    /api/menus/all/${categoryId} 엔드포인트는 storeId를 고려하지 않았다. 따라서 categoryId만으로 데이터를 반환하며, 다른 스토어의 데이터가 섞여 들어왔다. 예를 들어, storeId: 3에서 로드한 categoryId: 4의 메뉴가 storeId: 4에서도 재사용되었다.
  3. 상태 초기화 후 캐시 복원 누락:
    결제 후 resetData()menuCache를 초기화했지만, /pos로 돌아올 때 캐시를 복원하거나 새로 로드하는 로직이 없었다. PosPageuseEffectstoreId 변경에만 반응하도록 설계되어, 이미 설정된 storeId로는 데이터 로드가 스킵되었다.

해결 과정

문제를 해결하기 위해 몇 가지 단계를 거쳤다.

1. fetchMenusByCategorystoreId 추가

API 호출 시 storeId를 명시적으로 전달하도록 수정했다. 이를 통해 스토어별로 정확한 데이터를 가져오게 했다:

fetchMenusByCategory: async (categoryId: number, forceReload: boolean = false) => {
  const { storeId, menuCache } = get();
  if (!storeId) return;

  set({ isLoading: true });
  const storeCache = menuCache[storeId] || {};
  if (!forceReload && storeCache[categoryId]) {
    console.log(`Using cached menus for storeId: ${storeId}, categoryId: ${categoryId}`);
    set({ currentMenus: storeCache[categoryId], isLoading: false });
    return;
  }
  try {
    console.log(`Fetching menus from API for storeId: ${storeId}, categoryId: ${categoryId}`);
    const { data } = await axiosInstance.get(`/api/menus/all/${categoryId}`, {
      params: { storeId },
    });
    console.log(`API response for /api/menus/all/${categoryId}?storeId=${storeId}:`, data);
    const transformed = data.map((menu: any) => ({
      ...menu,
      menuId: menu.menuId ?? menu.id ?? null,
    }));
    set((state) => ({
      menuCache: {
        ...state.menuCache,
        [storeId]: {
          ...(state.menuCache[storeId] || {}),
          [categoryId]: transformed,
        },
      },
      currentMenus: transformed,
      isLoading: false,
    }));
  } catch (err) {
    console.error(`fetchMenusByCategory error for categoryId: ${categoryId}, storeId: ${storeId}:`, err);
    set({ currentMenus: [], isLoading: false });
  }
},

params: { storeId }를 추가해 백엔드가 storeId를 필터링하도록 했다.

2. 캐시 로직 점검

forceReloadtrue일 때 캐시를 무시하고 항상 새 데이터를 가져오도록 로직을 명확히 했다. 디버깅 로그(console.log)를 추가해 캐시 사용 여부와 API 응답을 확인했다.

3. resetData 범위 조정

결제 후 상태를 초기화하는 resetData를 수정했다. 모든 상태를 초기화하지 않고, 결제와 관련된 상태만 리셋하도록 했다:

resetData: () => {
  set({
    selectedItems: [],
    orderId: null,
    placeId: null,
    tableName: "",
    // storeId, categories, currentMenus는 유지
  });
},

이렇게 하면 storeIdmenuCache가 유지되어 /pos로 돌아올 때 캐시를 재사용할 수 있다.

 

 

4. PosPage에서 데이터 로드 보장

PosPageuseEffect를 개선해 페이지 진입 시 항상 데이터를 로드하도록 했다:

useEffect(() => {
  const savedStoreId = localStorage.getItem("currentStoreId");
  const newStoreId = savedStoreId ? Number(savedStoreId) : null;

  if (newStoreId) {
    console.log("Initializing or restoring storeId:", newStoreId);
    if (newStoreId !== storeId) {
      setStoreId(newStoreId);
    }
    fetchCategories(newStoreId).then(() => {
      const state = usePosStore.getState();
      const firstCategoryId = state.categories[0]?.categoryId;
      if (firstCategoryId && firstCategoryId !== selectedCategoryId) {
        setSelectedCategoryId(firstCategoryId);
      }
    });
  }
}, [setStoreId, fetchCategories]);
newStoreId !== storeId 조건을 유지하되, newStoreId가 존재하면 항상 fetchCategories를 호출하도록 했다.
 
 

결과

수정 후 로그를 확인하니 문제가 해결되었다:

 
Initializing or restoring storeId: 3
Fetching categories for storeId: 3
API response for /api/categories/all/3: Array(1)
Categories loaded: Array(1)
Setting selectedCategoryId to: 4
Fetching menus from API for storeId: 3, categoryId: 4
API response for /api/menus/all/4?storeId=3: Array(1)
PosPage - storeId: 3
PosPage - categories: Array(1)
PosPage - selectedCategoryId: 4
PosPage - currentMenus: Array(1)

 

스토어 전환 시 currentMenus가 정상적으로 로드되었고, 결제 후 /pos로 돌아와도 카테고리와 메뉴가 유지되었다.

 

 

회고

이 문제를 해결하면서 Zustand의 캐시 관리와 상태 초기화에 대해 깊이 고민하게 되었다. 처음에는 캐시 로직이 편리할 거라 생각했지만, storeId와 같은 컨텍스트가 바뀔 때 캐시가 오히려 방해가 될 수 있음을 깨달았다. 디버깅 과정에서 로그를 추가한 것이 큰 도움이 되었고, API와 프론트엔드 간의 데이터 일관성을 유지하는 것이 얼마나 중요한지도 다시 느꼈다. 앞으로는 캐시를 사용할 때 더 명확한 컨텍스트 관리와 상태 복원 로직을 설계해야겠다고 다짐했다. 이번 경험은 단순한 버그 수정 이상으로, 상태 관리의 본질을 되새기는 계기가 되었다.

반응형