시작하며...
현재 프로젝트의 상태 관리 라이브러리로 Zustand 를 사용하기로 했다.
사용하기 간편한 상태 관리 라이브러리 중에서 꾸준히 업데이트되면서 다운로드 수가 많은 Zustand 를 선택하게 되었다.
내용이 좀 빈약하지만 블로그에 간단하게 Zustand 를 사용하는 방법에 대해 글을 작성했었다.
https://jjang-j.tistory.com/57
persist 사용하게 된 이유
아래 코드와 같이 Zustand 코드를 작성하였고
import { create } from 'zustand';
import { getCookie, deleteCookie } from '@/utils/cookieUtil';
export type userInfo = {
id: number;
name: string;
};
interface AuthProps {
user: userInfo | null; // 유저 정보
saveUser: (user: userInfo) => void; // 유저 정보 저장
isLogin: boolean; // 로그인 여부
checkLogin: () => void; // 로그인 여부 확인 함수
logout: () => void; // 로그아웃
}
export const useAuthStore = create<AuthProps>((set) => {
return {
user: null,
isLogin: false,
saveUser: (user) => {
set({ user, isLogin: true });
},
checkLogin: () => {
const accessToken = getCookie('accessToken');
if (accessToken) {
set({ isLogin: true });
} else {
set({ user: null, isLogin: false });
}
},
logout: () => {
deleteCookie('accessToken');
set({ user: null, isLogin: false });
},
};
});
게시물 등록하기 페이지에서 현재 사용자의 닉네임을 가져와야 하는데 API 요청을 하기엔 불필요하다 생각이 들어 Zustand 에서 로그인할 때 저장한 user 의 name 을 가져오려고 했다.
그런데 로그인 후 게시물 등록하기 페이지에 들어오면 내 닉네임이 잘 보이는데 새로고침을 하면 닉네임의 상태값이 사라지게 된다. 🤣 그래서 로컬 스토리지에 상태값을 저장하기로 했다.
persist 란?
이때 간편하게 스토리지에 전역 상태 값을 저장하기 위해 persist 라는 미들웨어를 사용할 수 있다.
persist
Persist 미들웨어를 사용하면 Zustand 상태를 저장소(예: localStorage, AsyncStorage, IndexedDB 등)에 저장하여, 해당 데이터를 유지할 수 있음
[출처: zustand 깃허브 - persist]
persist 사용하여 코드 작성
persist 미들웨어를 사용하여 로컬스토리지에 authStore 라는 키값으로 저장하도록 아래 코드처럼 작성하였다.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { getCookie, deleteCookie } from '@/utils/cookieUtil';
export type userInfo = {
id: number;
name: string;
};
interface AuthProps {
user: userInfo | null; // 유저 정보
saveUser: (user: userInfo) => void; // 유저 정보 저장
isLogin: boolean; // 로그인 여부
checkLogin: () => void; // 로그인 여부 확인 함수
logout: () => void; // 로그아웃
}
export const useAuthStore = create(
persist<AuthProps>(
(set) => {
return {
user: null,
isLogin: false,
saveUser: (user) => {
set({ user, isLogin: true });
},
checkLogin: () => {
const accessToken = getCookie('accessToken');
if (accessToken) {
set({ isLogin: true });
} else {
set({ user: null, isLogin: false });
}
},
logout: () => {
deleteCookie('accessToken');
set({ user: null, isLogin: false });
},
};
},
{
name: 'authStore',
getStorage: () => {
return localStorage;
},
},
),
);
로컬스토리지 저장
로그인을 하고 위에서 만든 saveUser 를 사용하여 사용자 정보를 로컬 스토리지에 정상적으로 저장이 되었다. 😁
오류 발생
그런데 user 값을 사용하는 페이지에 가니 아래와 같은 오류를 발견하였다.
Text content does not match server-rendered HTML
Hydration Error
위에 에러를 읽어보면 서버에서 렌더링된 HTML과 클라이언트에서 렌더링된 HTML이 일치하지 않다고 한다.
즉, 서버 데이터 !== 클라이언트 데이터
라는 뜻이다.
Zustand 데이터를 클라이언트 측에서 생성하는 상태이므로, 서버에서는 해당 상태를 알 수 없다. 그래서 초기 렌더링을 하면 서버에서는 Zustand 상태값을 없는 상태로 시작을 하는 반면, 클라이언트 측에서는 스토리지에 저장된 값을 사용하다 보니 서버에서 전달받은 상태와 일치하지 않아 Hydraion Error 가 발생한다고 한다. (어렵다...🥲)
오류 해결
구글링을 통해 Hydration Error 를 해결하는 방법을 찾았다.
https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5
useStore 코드 작성
위에 링크를 참고하여 다음과 같이 useStore.ts
코드를 작성했고 useEffect 를 통해 서버 사이드에서 상태를 가져와 로컬에 저장된 상태값으로 업데이트해 주게 된다.
import { useState, useEffect } from 'react';
export const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F;
const [data, setData] = useState<F>();
useEffect(() => {
setData(result);
}, [result]);
return data;
};
useStore 사용하기
사용 방법은 다음과 같다. 위에서 새로 만든 useStore 와 사용할 스토어(useAuthStore) 를 import 한다. 그리고 useStore 의 첫 번째 인자로는 스토어를, 두 번째 인자로는 상태값을 받게 된다.
import { useAuthStore } from '@/store/userAuthStore';
import { useStore } from '@/store/useStore';
...
const Test = () => {
const user = useStore(useAuthStore, (state) => {
return state.user;
});
...
}
그러면 Hydration Error 가 발생하지 않고 로컬 스토어에 저장된 값을 정상적으로 사용할 수 있게 된다.
유의점
새로 만든 커스텀 useStore 를 모든 곳에서 사용하는 것은 아니다. 전역 상태 값을 바꿔주는 것들에는 사용하지 않고 상태 값을 가져오는 경우에만 사용이 된다. 그래서 아래 코드에서는 user, isLogin 값을 사용할 때에만 커스텀 useStore 를 사용해 주고, 나머지 saveUser, checkLogin, logout 에는 원래 사용법대로 사용하면 된다.
export const useAuthStore = create(
persist<AuthProps>(
(set) => {
return {
user: null, // 커스텀 useStore 사용
isLogin: false, // 커스텀 useStore 사용
saveUser: (user) => { // 기존 방식대로
set({ user, isLogin: true });
},
checkLogin: () => { // 기존 방식대로
const accessToken = getCookie('accessToken');
if (accessToken) {
set({ isLogin: true });
} else {
set({ user: null, isLogin: false });
}
},
logout: () => { // 기존 방식대로
deleteCookie('accessToken');
set({ user: null, isLogin: false });
},
};
},
{
name: 'authStore',
getStorage: () => {
return localStorage;
},
},
),
);
이런 식으로 사용하면 된다.
const { checkLogin } = useAuthStore();
const isLogin = useStore(useAuthStore, (state) => {
return state.isLogin;
});
마치며...
Zustand에서 persist 미들웨어를 사용하여 로컬 스토리지에 데이터를 저장하는 법에 대해 이해하게 되었다. 특히 Next.js 는 SSR 방식으로 발생할 수 있는 서버와 클라이언트 간의 데이터 불일치로 인해 Hydration Error 가 발생한다는 것을 알게 되었다.
이전 프로젝트에서도 Zustand 를 사용하였지만 내가 맡은 부분에는 클라이언트 측에서 전역으로 상태를 관리할 부분이 없어 이를 적용해보지 못해 아쉬웠는데 이번에 직접 내가 적용해 보게 되어 만족스러웠다. 😊
'💜 리액트 > Next.js' 카테고리의 다른 글
[Next.js] 메타태그, 오픈그래프 컴포넌트 SEO 향상 (feat. Page Router) (0) | 2024.07.12 |
---|---|
[Next.js] Framer Motion 화면 전환 애니메이션 적용 (feat. Page Router) (7) | 2024.07.10 |
[Next.js] create-next-app 없이 프로젝트 생성하기 (0) | 2024.06.21 |
[Next.js] SVG 컴포넌트 사용하는 방법 (React 보다 짱편함!) (0) | 2024.06.04 |
[Next.js] React 대신 Next.js 사용하는 이유?!? (1) | 2024.06.03 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!