[React / ts] Book Store 만들기 - ⑦ Zustand를 활용한 로그인 구현
https://www.npmjs.com/package/zustand
zustand
🐻 Bear necessities for state management in React. Latest version: 5.0.3, last published: 3 months ago. Start using zustand in your project by running `npm i zustand`. There are 3665 other projects in the npm registry using zustand.
www.npmjs.com
Zustand는 간결하게 상태 관리를 할 수 있도록 도와주는 라이브러리이다.
npm install zustand
// authStore.ts
import { create } from "zustand";
interface StoreState {
isLoggedIn: boolean;
storeLogin: (token: string) => void;
storeLogout: () => void;
}
export const getToken = () => {
const token = localStorage.getItem("token");
return token;
};
const setToken = (token: string) => {
localStorage.setItem("token", token);
};
export const removeToken = () => {
localStorage.removeItem("token");
};
export const useAuthStore = create<StoreState>((set) => ({
isLoggedIn: getToken() ? true : false,
storeLogin: (token: string) => {
set({ isLoggedIn: true });
setToken(token);
},
storeLogout: () => {
set({ isLoggedIn: false });
removeToken();
},
}));
위와 같이 Zustand 코드를 작성해준다.
create<StoreState>()를 통해 useAuthStore를 만들고, 이를 통해 StoreState의 isLoggedIn, storeLogin, storeLogout을 사용할 수 있도록 하였다.
isLoggedIn을 통해 현재 로그인 되어있는지의 여부를 알 수 있고,
storeLogin을 통해 로그인되었음을 설정함과 토큰을 localStorage에 저장하고,
storeLogout을 통해 로그아웃되었음을 설정함과 토큰을 localStorage에서 삭제하도록 하였다.
// auth.api.ts
interface LoginResponse {
token: string;
}
export const login = async (userData: SignupProps) => {
const response = await httpClient.post<LoginResponse>(
"/users/login",
userData
);
return response.data;
};
로그인을 실행하는 api 함수이다.
// Login.tsx
import Title from "../components/common/Title";
import InputText from "../components/common/InputText";
import { Link, useNavigate } from "react-router-dom";
import Button from "../components/common/Button";
import { useForm } from "react-hook-form";
import { login } from "../api/auth.api";
import { useAlert } from "../hooks/useAlert";
import { SignupProps, SignupStyle } from "./Signup";
import { useAuthStore } from "../store/authStore";
const Login = () => {
const navigate = useNavigate();
const showAlert = useAlert();
const { storeLogin } = useAuthStore();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupProps>();
const onSubmit = (data: SignupProps) => {
login(data).then(
(res) => {
storeLogin(res.token);
showAlert("로그인에 성공하였습니다");
navigate("/");
},
(error) => {
showAlert("로그인에 실패하였습니다.");
}
);
};
return (
<>
<Title size="large">로그인</Title>
<SignupStyle>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<InputText
placeholder="이메일"
inputType="email"
{...register("email", { required: true })}
/>
{errors.email && <p className="error-text">이메일을 입력해세요</p>}
</fieldset>
<fieldset>
<InputText
placeholder="비밀번호"
inputType="password"
{...register("password", { required: true })}
/>
{errors.password && (
<p className="error-text">비밀번호를 입력해세요</p>
)}
</fieldset>
<fieldset>
<Button type="submit" size="medium" scheme="primary">
로그인
</Button>
</fieldset>
<div className="info">
<Link to="/reset">비밀번호 초기화</Link>
</div>
</form>
</SignupStyle>
</>
);
};
export default Login;
로그인 컴포넌트이다.
onSubmit에서 로그인 로직을 처리해주는데, 로그인에 성공하는 경우 Zustand의 storeLogin(res.token)을 통해 상태를 업데이트해준다.
로그인에 성공한 경우, localStorage에 token이 정상적으로 반영된 것을 볼 수 있다.
// Header.tsx
import { styled } from "styled-components";
import logo from "../../assets/images/maggie.png";
import { FaRegUser, FaSignInAlt } from "react-icons/fa";
import { Link } from "react-router-dom";
import { useCategory } from "../../hooks/useCategory";
import { useAuthStore } from "../../store/authStore";
const Header = () => {
const { category } = useCategory();
const { isLoggedIn, storeLogout } = useAuthStore();
return (
<>
<HeaderStyle>
<h1 className="logo">
<Link to="./">
<img src={logo} alt="book store"></img>
</Link>
</h1>
<nav className="category">
<ul>
{category.map((item) => (
<li key={item.category_id}>
<Link
to={
item.category_id === null
? `/books`
: `/books?category_id=${item.category_id}`
}
>
{item.category_name}
</Link>
</li>
))}
</ul>
</nav>
<nav className="auth">
{isLoggedIn ? (
<ul>
<li>
<Link to="/cart">장바구니</Link>
</li>
<li>
<Link to="/orderlist">주문 내역</Link>
</li>
<li>
<button onClick={storeLogout}>로그아웃</button>
</li>
</ul>
) : (
<ul>
<li>
<Link to="/login">
<FaSignInAlt /> 로그인
</Link>
</li>
<li>
<Link to="/signup">
{" "}
<FaRegUser />
회원가입
</Link>
</li>
</ul>
)}
</nav>
</HeaderStyle>
</>
);
};
const HeaderStyle = styled.header`
width: 100%;
margin: 0 auto;
max-width: ${({ theme }) => theme.layout.width.large};
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid ${({ theme }) => theme.color.background};
.logo {
img {
width: 100px;
}
}
.category {
ul {
display: flex;
gap: 32px;
li {
a {
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
color: ${({ theme }) => theme.color.text};
&:hover {
color: ${({ theme }) => theme.color.primary};
}
}
}
}
}
.auth {
ul {
display: flex;
gap: 16px;
li {
a,
button {
font-size: 1rem;
font-weight: 600;
text-decoration: none;
display: flex;
align-items: center;
line-height: 1;
background: none;
border: none;
cursor: pointer svg {
margin-right: 6px;
}
}
}
}
}
`;
export default Header;
로그아웃도 storeLogout을 통해 간단하게 설정할 수 있다.