웹 풀스택

[React / ts] Book Store 만들기 - ⑦ Zustand를 활용한 로그인 구현

kevinmj12 2025. 4. 23. 22:11

 

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을 통해 간단하게 설정할 수 있다.