상태를 지키는 힘 : zustand + persist로 전역·영속 관리하기

YuminPark·2025년 8월 30일

veco

목록 보기
4/5
post-thumbnail

워크스페이스 생성 기능을 리팩토링하면서, 생각보다 여러 디테일이 필요하다는 걸 알게 되었다.
단순히 입력값을 넣어서 서버에 요청만 하면 끝날 줄 알았는데… 실제 안정적인 서비스로 동작하려면 페이지 이동, 새로고침, 중복 요청 방지까지 챙겨야 했다. 이번 글에서는 그 과정을 정리해보려고 한다.


기본 기능은 무엇이었나?

처음에는 그래도 단순했다. 간략하게 말하자면 사용자가 워크스페이스 이름을 입력하고, 체크 버튼을 누르면 유효성 검사를 실시하게 된다. 이 검사를 통과하면 서버에 요청을 보낸다. 제대로 된 서버 요청에 성공하면 입력창은 잠기고(readOnly), 사용자가 이용 가능한 워크스페이스 url이 화면에 보이게 된다.

여기까지만 보면 그냥 로컬 상태(useState)로도 충분해 보였다. 굳이 전역으로 관리하지 않아도 되는 것들이니까!

새로고침 / 페이지 이동 시 값이 사라지는 문제

문제는 바로 여기서 시작됐다.
사용자가 체크 버튼을 눌러 url까지 확인 했는데, 만약 페이지를 이동했다가 다시 돌아오면? 또는 단순히 새로고침을 하면? 👉🏻 입력값이 싹 다 날아가는 것이었다....ㅠㅠ

이건 서비스 정책상 치명적이다. 🥹 우리 서비스에서는 계정당 워크스페이스를 하나만 만들 수 있기 때문!
만약 이미 워크스페이스 생성까지 했는데 값이 날아가면, 사용자가 다시 입력 → 체크 버튼 → 생성하기 버튼을 누르다가 에러가 나는 상황이 발생한다. 서버 입장에서는 이 사용자가 이미 워크스페이스를 만든 이력이 있기 때문이다.

즉, 단순히 컴포넌트 로컬 상태로 관리하면 안 되고, 전역 + 영속적 관리가 필요했다!

zustand + persist로 전역 관리

그래서 꺼내든 무기는 zustand + persist 조합! 🤩
stores/onboardingWorkspace.ts라는 파일을 만들어, 이름/URL/잠금 여부를 전역 상태로 관리하고자 했다.

export type WSState = {
  workspaceName: string;
  workspaceUrl: string;
  isLocked: boolean;
  setWorkspaceName: (v: string) => void;
  setWorkspaceUrl: (v: string) => void;
  setIsLocked: (v: boolean) => void;
};

export const useOnboardingWS = create<WSState>()(
  persist(
    (set) => ({
      workspaceName: '',
      workspaceUrl: '',
      isLocked: false,
      setWorkspaceName: (v) => set({ workspaceName: v }),
      setWorkspaceUrl: (v) => set({ workspaceUrl: v }),
      setIsLocked: (v) => set({ isLocked: v }),
    }),
    {
      name: 'onboarding-workspace',
      storage: createJSONStorage(() => localStorage),
      partialize: (s) => ({
        workspaceName: s.workspaceName,
        workspaceUrl: s.workspaceUrl,
        isLocked: s.isLocked,
      }),
    }
  )
);

여기서 중요한 포인트는 partialize이다.
사용자가 입력한 워크스페이스명, 체크 후 얻은 url, 그리고 잠금 여부까지 로컬스토리지에 키로 저장한다.
잠금 여부까지 저장한 이유는, 체크 통과 후에도 사용자가 입력을 이어가면 불필요하게 로컬스토리지 값이 덮어써지는 문제를 막기 위함이다!

readOnly + onChange 가드

실제 입력창에는 이렇게 적용했다 :

// 코드 일부 생략
<input
  type="text"
  placeholder="워크스페이스 이름"
  value={workspaceName}
  onChange={(e) => {
    if (isLocked || !!workspaceUrl) return; // 잠금 또는 URL 있으면 무시
    setWorkspaceName(e.target.value);
  }}
  readOnly={isLocked || !!workspaceUrl}
/>
  • 체크 버튼을 누르면 isLocked = true
  • 그 이후에는 입력창이 수정 불가능(readOnly)
  • 동시에 onChange에서도 한 번 더 가드

👉 이렇게 하면 잠금 상태에서도 혹시 모를 값 변경 시도가 깔끔히 차단된다!

생성하기 중복 요청 문제

여기서 끝났으면 좋았겠지만… 😅 한 번 더 생각해봐야 할 문제가 있었다.
사용자가 이미 워크스페이스를 생성한 상태인데, 뒤로가기를 눌러 다시 이 화면에 돌아와 '워크스페이스 생성하기' 버튼을 누른다면? 👉🏻 이 역시도 에러가 난다.
이걸 막지 않으면, 사용자는 한 번이라도 새로고침을 하거나 페이지 이동을 하면 에러 때문에 더 이상 앞으로 이동하지 못하는 상황이 생겨버린다.

생성 이력도 persist로 관리

그래서 stores/onboardingStatus.ts라는 전역 스토어를 하나 더 만들었다.
이번에는 생성 이력 자체를 persist로 관리한다.

export const useOnboardingStatus = create<OnboardingStatusState>()(
  persist(
    (set) => ({
      workspaceCreated: false,
      createdWorkspaceId: undefined,
      setWorkspaceCreated: (v, id) => set({ workspaceCreated: v, createdWorkspaceId: id }),
      resetWorkspaceCreated: () => set({ workspaceCreated: false, createdWorkspaceId: undefined }),
    }),
    {
      name: 'onboarding-status',
      storage: createJSONStorage(() => localStorage),
      partialize: (s) => ({
        workspaceCreated: s.workspaceCreated,
        createdWorkspaceId: s.createdWorkspaceId,
      }),
    }
  )
);

이제 workspaceCreated가 true라면, 다시 버튼을 눌러도 API 호출 없이 바로 다음 단계로 이동시킨다.


const { workspaceName, workspaceUrl, isLocked, setWorkspaceName, setWorkspaceUrl, setIsLocked } = useOnboardingWS();
const { workspaceCreated, setWorkspaceCreated } = useOnboardingStatus();
// 코드 일부 생략..

const handleButtonClick = async () => {
  if (workspaceCreated) {
    navigate('/onboarding/invite');
    return;
  }

  try {
    await mutateAsync({ workspaceName });
    setWorkspaceCreated(true);
    navigate('/onboarding/invite');
  } catch (error) {
    console.error('에러 발생:', error);
  }
};

정리 및 느낀 점

  • zustand + persist 조합 덕분에 전역 상태를 안정적으로 관리할 수 있었다. 또한 partialize를 사용해서 간편하게 로컬스토리지에 접근할 수 있다는 것도 굉장한 장점인 것 같다.

  • 온보딩 플로우가 훨씬 견고해졌다! 새로고침을 하든, 페이지를 이동했다가 돌아오든, 혹은 이미 워크스페이스를 만든 계정이든... 어떤 경우에도 일관된 동작을 보장할 수 있게 되었다 ㅎㅎ♪(´▽`)

  • zustandRedux Toolkit에 비해 코드가 훨씬 간결하고 가볍다. 정말...!! UMC 8기 Web 워크북으로 공부할 때마다 느끼던 장점이었는데, 실제 프로젝트에 적용하면서 그 차이를 다시 한번 확실히 체감할 수 있었다.

profile
기억하고 싶은 것을 기록합니다

0개의 댓글