728x90
0.백엔드 엔드포인트코드
해당 글작성에 사용된 JWT 로그인 기능 백엔드 코드에 관한 설명은 다음 글에 작성되어있고, 해당 글에 작성된 코드의 엔드포인트를 기준으로 작성했습니다.
https://asa9874.tistory.com/663
[SpringBoot] 쇼핑몰 JWT와 로그인,로그아웃 기능 제작 (2)
1.JWT인증과 정보 교환을 위해 사용되는 토큰 기반 인증 방식중 하나로 로그인 기능에 JWT(JSON Web Token)을 사용할거다.그를 위해 일단은 JWT사용을 위한 의존성을 추가해준다. io.jsonwebtoken jjwt-api 0.12.
asa9874.tistory.com
1.로그인 api 함수
나는 이번에 JWT 토큰을 프론트엔드 부분에서 받는 로직을 LocalStorage를 사용하여 구현하였다.
다음 함수는 로그인 엔드포인트에 매개변수로 받은 아이디와 비밀번호를 POST 해주고 만약 응답이 정상적으로 오게 되었다면 응답값을 리턴해준다.
import { BASE_URL } from "../../Home/context/baseURL";
export async function loginMember(memberData: { memberId: string; password: string }) {
try {
const response = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(memberData),
});
if (!response.ok) {
const errorMessage = await response.text();
throw new Error(`로그인 실패: ${errorMessage || "아이디 또는 비밀번호를 확인하세요."}`);
}
const responseData = await response.json();
if (!responseData?.token) {
throw new Error("로그인 응답에 토큰이 없습니다.");
}
return responseData;
} catch (error) {
console.error("로그인 오류:", error instanceof Error ? error.message : String(error));
throw error;
}
}
2.로그인 컴포넌트
Form을 통해 아이디 비밀번호를 입력받고 로그인을 시도하면 위의 api 함수를 통해 응답값을 가져오고
localStorage에 token을 응답의 토큰값으로 설정하였다.
function Login() {
const { setNickname } = useAuthStore();
const [formData, setFormData] = useState({ memberId: "", password: "" });
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const responseData = await loginMember(formData);
localStorage.setItem("token", responseData.token);
navigate("/");
} catch (error) {
console.error("로그인 실패:", error);
}
};
return (
<div className="min-h-screen bg-gray-100 flex justify-center items-center">
<form onSubmit={handleSubmit} className="w-full max-w-md bg-white p-6 rounded shadow-md">
<h2 className="text-2xl font-bold text-center text-gray-700 mb-6">로그인</h2>
<div className="mb-4">
<label htmlFor="memberId" className="block text-sm font-medium text-gray-700">아이디</label>
<input
type="text"
id="memberId"
name="memberId"
value={formData.memberId}
onChange={(e) => setFormData({ ...formData, memberId: e.target.value })}
required
className="mt-1 p-2 border border-gray-300 rounded w-full"
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">비밀번호</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
className="mt-1 p-2 border border-gray-300 rounded w-full"
/>
</div>
<div className="flex justify-center">
<button type="submit" className="px-6 py-2 bg-blue-500 text-white font-bold rounded hover:bg-blue-700">
로그인
</button>
</div>
</form>
</div>
);
}
3.zustand 전역 상태
zustand로 전역으로 닉네임을 관리하며 로컬스토리지의 token을 지우고 nickname 상태를 null로 만드는 logout 함수또한 관리한다.
import { create } from "zustand";
interface AuthState {
nickname: string | null;
setNickname: (nickname: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
nickname: null,
setNickname: (nickname) => set({ nickname }),
logout: () => {
localStorage.removeItem("token");
set({ nickname: null });
},
}));
4.로그인 상태일때 관리
헤더에서 로그인 상태일때 로그아웃 버튼과 닉네임이 뜨도록 관리하도록 구현하였다.
닉네임의 경우 token이 존재할때만 api를 불러오게 만들었으며 해당 nickName은 zustand store 에서 관리하게 만든다.
또한 이렇게 닉네임이 존재하는 경우 로그아웃과 닉네임이 헤더에 뜨며, 로그아웃을 누르면 token과 nickname 값이 사라진다.
import { useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { BASE_URL } from "../pages/Home/context/baseURL";
import { useAuthStore } from "../store/useAuthStore";
function Header() {
const { nickname, setNickname, logout } = useAuthStore();
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) return;
fetchNickname(token);
}, []);
const fetchNickname = async (token: string) => {
try {
const response = await fetch(`${BASE_URL}/member/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
setNickname(data.nickname);
} else {
console.error("닉네임 불러오기 실패");
logout();
}
} catch (error) {
console.error("API 요청 오류:", error);
logout();
}
};
const handleLogout = () => {
logout();
navigate("/");
};
return (
<div className="h-[60px] bg-gray-800 flex items-center justify-between px-6 text-white">
<Link to="/">
<button className="bg-blue-500 px-4 py-2 rounded hover:bg-blue-700">홈</button>
</Link>
{nickname ? (
<div className="flex items-center gap-4">
<span className="font-bold">{nickname}님</span>
<button onClick={handleLogout} className="bg-red-500 px-4 py-2 rounded hover:bg-red-700">
로그아웃
</button>
</div>
) : (
<div className="flex gap-4">
<Link to="/register">
<button className="bg-blue-500 px-4 py-2 rounded hover:bg-blue-700">회원가입</button>
</Link>
<Link to="/login">
<button className="bg-blue-500 px-4 py-2 rounded hover:bg-blue-700">로그인</button>
</Link>
</div>
)}
</div>
);
}
export default Header;
728x90
'FrontEnd > React' 카테고리의 다른 글
[React] 사용하지않는 패키지 제거 depcheck 사용법 (0) | 2025.02.07 |
---|---|
[React] ESLint와 Prettier란? , 사용법 (0) | 2025.02.07 |
[React] Netlify 로 배포해서 APIKEY 숨기기 (0) | 2025.02.02 |