Zustand + Persist?? 운동 많이 된다

gangmin·2026년 1월 25일

React

목록 보기
7/9
post-thumbnail

“분명 저장됐는데, 왜 새로고침하면 초기화될까?”

분명 로컬 스토리지에는 값이 있다.
개발자 도구를 열어보면 내가 바꾼 설정이 아주 멀쩡하게 들어 있다.

그런데 새로고침을 누르는 순간, UI는 아무 일도 없었다는 듯 다시 초기 상태로 돌아간다.

“음…? 내가 뭘 놓친 거지?”

처음엔 hydrate 타이밍 문제인가 싶었다. useEffect에서 초기화를 한 건 아닌지, React Strict Mode 때문인지, zustand가 초기 state를 먼저 써버리는 건 아닌지…

별의별 가능성을 다 의심해봤다. 그런데 이상했다.

저장은 되는데, 복원만 안 된다. 😵

1. 문제의 시작: “설정은 settings 안에 모아두면 깔끔하잖아?”

대시보드 앱에서 유저 워크스페이스 설정을 관리하는 스토어를 만들었다고 가정하자. 😉
사이드바 접힘 여부, 테마(light/dark) 같은 값들이다.

구조적으로 깔끔해 보이도록, 모든 설정을 settings라는 객체 안에 모아두었다.

type WorkspaceStore = {
  state: {
    settings: {
      sidebarCollapsed: boolean;
      theme: 'light' | 'dark';
    };
  };
  actions: {
    toggleSidebar: () => void;
    setTheme: (theme: 'light' | 'dark') => void;
  };
};

persist도 자연스럽게 붙였다.

persist(
  (set) => ({
    state: {
      settings: {
        sidebarCollapsed: false,
        theme: 'light',
      },
    },
    actions: { ... },
  }),
  {
    name: 'WORKSPACE_STORAGE',
    partialize: (state) => ({ state: state.state }),
  }
);

이 시점에서는 아무 문제도 없었다. 새로고침해도 상태는 잘 유지됐다.

2. 코드 리뷰: “state가 중복된 것 같아요”

문제는 여기서 시작됐다. 코드 리뷰에서 이런 피드백을 받았다.

state.state 구조가 조금 이상해 보여요. state 중복을 없애는 게 좋지 않을까요?”

나도 그 말에 바로 수긍했고 다음처럼 바꿨다.

partialize: (state) => state.state

중복을 제거했고, 코드도 훨씬 깔끔해 보였다.

그리고 바로, 문제가 시작됐다... 😈

3. 다시 나타난 증상: 새로고침하면 초기화

이제는 이런 현상이 발생했다.

  • 설정 변경 → localStorage 저장됨 ✅
  • 새로고침 → 상태 초기화 ❌
  • 에러 없음
  • hydrate도 도는 것 같음

이때가 제일 헷갈렸다.

“아니… 아까까지 잘 되던 게 state 중복만 없앴을 뿐인데 왜 망가졌지?”

여기서 중요한 포인트는 이거다.

리뷰는 맞았고, 수정도 논리적으로 맞았지만 persist 관점에서는 치명적인 변경이었다.

4. 진짜 원인: Shape Mismatch

partialize: (state) => state.state 로 바꾸면서 localStorage에 저장되는 값은 이렇게 변했다.

{
  "settings": {
    "theme": "dark",
    "sidebarCollapsed": false
  }
}

하지만 Zustand persist는 이 값을 스토어의 최상위(root)에 얕게 병합한다. 결과적으로 hydrate 이후 내부 상태는 이런 모습이 된다.

{
  state: {
    settings: {
      theme: 'light',
      sidebarCollapsed: false,
    },
  },
  actions: { ... },
  settings: {
    theme: 'dark',
    sidebarCollapsed: false,
  },
}

그리고 나는 계속 store.state.settings.theme만 보고 있었다.

값은 복원됐지만, 내가 보고 있는 위치가 아니었다. 💡

이게 바로 “저장은 되는데, 복원이 안 되는 것처럼 보였던” 이유였다.

5. 결론: persist를 쓰는 순간, 구조 전략이 바뀐다

이쯤 와서 깨달았다.

이건 partialize의 문제가 아니라,
persist + state/actions 패턴의 궁합 문제라는 걸.

persist를 쓰는 스토어에서는 구조를 “논리적으로 예쁜 것”보다 “직렬화와 병합에 정직한 형태” 로 가져가는 게 중요했다.

그래서 결국 store를 flat 하게 바꿨다.

type WorkspaceStore = {
  sidebarCollapsed: boolean;
  theme: 'light' | 'dark';
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
};
persist(
  (set) => ({
    sidebarCollapsed: false,
    theme: 'light',
    toggleSidebar: () =>
      set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
    setTheme: (theme) => set({ theme }),
  }),
  { name: 'WORKSPACE_STORAGE' }
);

이후로는 GOOD 👍

  • 새로고침 문제 없음
  • partialize 고민 없음
  • hydrate 동작이 예측 가능

6. 이 경험에서 얻은 교훈

  1. partialize는 구조를 바꾸지 않는다
    → 단지 “어디에 저장할지”만 바꾼다.

  2. persist store는 flat 하게
    → 중첩 구조 + shallow merge = 디버깅 지옥

0개의 댓글