![[Next.js] Zustand persist 사용하기 (feat. Hydration 에러 해결)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcegsaM%2FbtsIibkjaBe%2FMgmY99gEJkOkjDquhmAO81%2Fimg.png)
시작하며...
현재 프로젝트의 상태 관리 라이브러리로 Zustand 를 사용하기로 했다.
사용하기 간편한 상태 관리 라이브러리 중에서 꾸준히 업데이트되면서 다운로드 수가 많은 Zustand 를 선택하게 되었다.
내용이 좀 빈약하지만 블로그에 간단하게 Zustand 를 사용하는 방법에 대해 글을 작성했었다.
https://jjang-j.tistory.com/57
[React] Zustand 상태 관리 라이브러리 사용 방법
시작하며...React 에서 다른 컴포넌트로 데이터를 전달하기 위해 props 를 사용한다.그러나 React 의 상태인 state 는 자식 컴포넌트한테만 전달할 수 있어 만약 컴포넌트가 많고 잘 분리되어 있을 경
jjang-j.tistory.com
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
How to use Zustand's persist middleware in Next.js
In this article, we'll discuss the common error that arises when using Zustand's persist middleware...
dev.to
useStore 코드 작성
위에 링크를 참고하여 다음과 같이 useStore.ts
코드를 작성하여 클라이언트에서 로컬 저장소의 상태를 가져와 업데이트하는 방식을 사용했다.
useEffect를 사용해 store에서 상태를 가져오고, 그 상태를 useState를 사용하여 로컬 상태로 설정한다. 그러면 클라이언트에서 초기 렌더링 시 서버에서 받은 상태와 일치하도록 도와주며, Hydration Error를 방지한다.
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 훅은 두 개의 인자를 받는다.
첫 번째 인자 store는 상태를 반환하는 함수이고, 두 번째 인자 callback은 store로부터 반환된 상태에서 필요한 값을 추출하는 함수다. 그러면 useEffect로 상태가 변경될 때마다 실행되어 로컬 상태를 업데이트하게 된다.
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 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!