새로고침하면 로그인이 풀리던 문제를 Zustand persist로 해결했다
새로고침하면 관리자 로그인이 풀리던 문제를 Zustand persist로 해결했다. 무엇을 저장하고 무엇은 저장하면 안 되는지, 토큰까지 localStorage에 들어간 지금 코드의 문제도 같이 적었다.
#프론트엔드 #zustand #상태관리 #React
관리자로 로그인해놓고 페이지를 새로고침했더니 로그인이 그냥 풀려버렸다. 다시 로그인 화면이다. React로 옮기면서 전역 상태는 Zustand를 썼는데, 가볍고 보일러플레이트가 거의 없어서 마음에 들었지만 이 부분이 초반에 걸렸다. 새로고침하면 상태가 전부 날아간다. 이 글에서는 이걸 persist 미들웨어 로 어떻게 해결했는지, 그리고 편하다고 아무거나 persist 했다가 뭘 겪었는지 정리해본다. 왜 날아가나 당연한 일이긴 하다. Zustand든 Redux든 전역 상태는 결국 메모리에 있는 자바스크립트 변수다. 새로고침하면 페이지가 통째로 다시 로드되면서 그 변수가 초기값으로 돌아간다. 그래서 상태도 같이 사라진다. 로딩 중이거나 일회성인 상태, 예를 들어 모달이 열려 있는지 같은 건 사라져도 상관없다. 오히려 새로고침했는데 모달이 그대로 떠 있으면 이상하다. 문제는 로그인 정보처럼 새로고침해도 유지돼야 하는 상태였다. 이건 메모리 바깥 어딘가에 적어뒀다가 다시 읽어와야 한다. persist 미들웨어 한 겹이면 된다 Zustand에는 persist라는 미들웨어가 있다. 스토어를 이걸로 감싸주면, 상태가 바뀔 때마다 자동으로 localStorage에 저장하고, 페이지가 로드될 때 거기서 자동으로 복원해준다. 내가 직접 localStorage를 건드릴 필요가 없다. // store/data.ts import { create } from "zustand"; import { persist } from "zustand/middleware"; const useAdminStore = create AdminState ()( persist( (set) = ({ admin: '', setAdmin: (newAdmin) = set({ admin: newAdmin }), resetAdmin: () = set({ admin: '' }), }), { name: 'admin-storage', // localStorage에 저장될 키 이름 } ) ); 평소 스토어 코드와 다른 건 persist(...)로 한 번 감싼 것과 name을 준 것뿐이다. 그런데 이걸로 admin 상태가 localStorage의 admin-storage 키에 자동 저장되고, 새로고침해도 복원된다. setAdmin을 부르면 알아서 저장까지 된다. 개발자 도구의 Application 탭을 열어보면 이런 값이 들어 있다. admin-storage: {"state":{"admin":"..."},"version":0} version이 같이 들어가는 건 나중에 상태 모양이 바뀌었을 때 옛 데이터를 변환하는 migrate 옵션을 위해서다. 아직 쓴 적은 없는데, 저장 형식을 바꿀 일이 생기면 이게 있어서 다행일 것 같다. 전부 persist하면 안 된다 처음엔 신나서 모든 스토어에 persist를 붙일 뻔했는데, 이건 좀 생각해봐야 한다. localStorage에 남는다는 건 브라우저를 닫았다 열어도 계속 남아 있다는 뜻이다. 그러니까 "유지돼야 하는 것"과 "항상 최신이어야 하는 것"을 나눠야 한다. 로그인이나 관리자 상태는 유지돼야 하니 persist를 붙였다. 반대로 지금 보고 있는 글 데이터나 잠깐 쓰는 상태는 매번 서버에서 새로 받아야 하니 persist를 안 붙였다. 이걸 안 나눴을 때 실제로 겪은 일이 있다. 글 목록을 persist 했더니, 새 글을 올렸는데 옛날 목록이 localStorage에서 복원돼서 새 글이 안 보였다. 서버에는 분명 있는데 화면에는 없으니 한참 헤맸다. 저장된 상태가 서버보다 오래된 것일 수 있다는 걸 그때 알았다. 지금 내 localStorage를 열어보면 persist가 편해서 로그인 정보를 통째로 넣었다. 이 블로그의 GitHub 댓글 로그인 스토어가 지금 이렇게 생겼다. // store/data.ts const useGithubUserStore = create GithubUserState ()( persist( (set) = ({ githubUser: null, githubToken: null, setGithub: (user, token) = set({ githubUser: user, githubToken: token }), resetGithub: () = set({ githubUser: null, githubToken: null }), }), { name: 'github-user-storage', } ) ); 저장할 항목을 고르는 partialize 옵션을 안 썼으니 githubToken까지 그대로 localStorage에 들어간다. 개발자 도구 Application 탭을 열면 평문으로 보인다. localStorage는 같은 도메인에서 도는 스크립트면 다 읽을 수 있다. 광고 스크립트를 붙여둔 사이트라 더 신경 썼어야 했는데, 편한 쪽으로 먼저 가버렸다. 당장 할 수 있는 건 partialize로 토큰을 빼고 사용자 정보만 남기는 거다. { name: 'github-user-storage', partialize: (state) = ({ githubUser: state.githubUser }), // 토큰은 저장하지 않는다 } 제대로 하려면 토큰은 httpOnly 쿠키로 옮겨야 한다. 서버에서 쿠키를 심고 CORS 설정도 같이 손봐야 하는 일이라 한 번에 묶어서 처리할 작업으로 잡아뒀다. 그 전까지는 토큰 만료를 짧게 가져가는 것만으로도 위험이 꽤 줄어든다. persist는 편한 만큼 무심코 다 저장하게 만든다. 붙일 때 "이게 브라우저를 닫아도 남아야 하나"와 "이게 남아 있어도 괜찮은 정보인가"를 한 번씩 물어보길 추천한다. 둘 중 하나라도 아니면 persist를 빼거나 partialize로 골라내는 게 맞다.