먼저 아래 화면은 관리자가 신고 목록을 처리할 수 있는 페이지이다.
이 페이지는 관리자로 로그인 했을 때 헤더에서 드롭 다운으로 들어갈 수 있다.
하지만 URL을 통해 일반 사용자나 비로그인 상태로 들어오려고 하면 막아야 한다.
http://localhost:3000/admin/report
어떻게 해야 할까
일단 내가 구현해 본 방식을 먼저 설명하고자 한다.
전역 상태 관리, 라우팅 및 보호된 경로 설정, 조건부 UI 렌더링을 다루며, 역할 기반 접근 제어를 어떻게 처리했는지 설명하겠다.
Zustand로 전역 상태 관리
애플리케이션의 상태 관리 라이브러리로 Zustand를 선택한 이유는 가볍고, 간단하며, 빠른 상태 관리가 가능하기 때문이다.
이번 프로젝트에서는 사용자 인증 상태를 관리하기 위해 Zustand를 사용했다.
useAuthStore 훅을 통해 애플리케이션 전역에서 사용자 인증 상태를 관리하고, 각 컴포넌트에서 쉽게 이 상태에 접근할 수 있다.
src/store/useAuthStore.js
import { create } from 'zustand';
const useAuthStore = create((set) => ({
isLogin: false,
token: '',
email: '',
name: '',
role: '',
userId: null,
login: (token, email, name, role, userId) => set({
isLogin: true,
token,
email,
name,
role,
userId: Number(userId) // 상태에서는 숫자로 저장
}),
logout: () => {
// localStorage 초기화
localStorage.clear();
// 상태 초기화
set({
isLogin: false,
token: '',
email: '',
name: '',
role: '',
userId: null
});
},
// [헤더] > OOO님
setName: (newName) => set(() => {
localStorage.setItem('name', newName);
return { name: newName };
}),
}));
export default useAuthStore;
- 로그인 정보 저장 및 초기화
: login 함수는 사용자가 로그인할 때 호출되어 상태를 업데이트하고, logout 함수는 로그아웃 상태와 localStorage를 초기화한다.
- 헤더 상태와 localStorage 동기화
: setName 함수는 상태와 localStorage를 동기화하여, 언제든지 이름 변경을 UI에 반영할 수 있게 한다.
React Router를 사용한 보호된 경로 설정
React Router를 사용해 애플리케이션의 페이지 이동을 관리하면서, 특정 경로에 대한 접근을 제한하는 ProtectedRoute 컴포넌트를 구현했다. 이 컴포넌트는 사용자의 로그인 상태와 역할을 기반으로 접근 권한을 제어한다.
src/common/authority/ProtectedRoute.js
import { Navigate } from "react-router-dom";
import useAuthStore from "../../store/useAuthStore";
const ProtectedRoute = ({ children, requiredRole }) => {
const { role, isLogin } = useAuthStore();
if (!isLogin) {
return <Navigate to="/authority" />;
}
if (role !== requiredRole) {
return <Navigate to="/authority" />;
}
return children;
};
export default ProtectedRoute;
- 역할 기반 접근 제어
: 사용자가 로그인되어 있지 않거나 사용자의 role이 requiredRole과 일치하지 않으면 접근이 제한된다. (ROLE_ADMIN)
- 정상적인 접근
: 사용자 역할이 ROLE_ADMIN이고, 로그인이 되어 있는 경우에만 보호된 경로의 컴포넌트를 렌더링한다.
UI에서의 역할 기반 조건부 렌더링
헤더 컴포넌트에서는 사용자의 로그인 상태와 역할에 따라 다른 UI 요소를 렌더링한다.
로그인되지 않은 사용자는 로그인 버튼을, 로그인된 사용자는 사용자 이름과 함께 드롭다운 메뉴를 확인할 수 있다.
src/layout/header/components/Header.jsx
import React, { useState } from "react";
import { NavLink, useLocation } from 'react-router-dom';
import useAuthStore from "../../../store/useAuthStore";
import useModalStore from "../../../store/useModalStore";
import "../styles/Header.css";
import layoutLogo from "../../logo/layoutLogo.png";
import arrow from "../assets/arrow.png";
const Header = () => {
// Zustand의 useModalStore 훅을 사용하여 가져옴
const openLoginModal = useModalStore((state) => state.openLoginModal);
// Zustand의 useAuthStore 훅을 사용하여 가져옴
const { isLogin, name, role, logout } = useAuthStore((state) => ({
isLogin: state.isLogin,
name: state.name,
role: state.role,
logout: state.logout
}));
// 현재 경로 확인을 위한 hook
const location = useLocation();
// 드롭다운 메뉴
const [showDropDownMenu, setShowDropDownMenu] = useState(false);
// 로그인을 했을 때 드롭다운 메뉴 : USER(마에페이지, 로그아웃) || ADMIN(신고목록, 로그아웃)
const handleShowDropDownMenu = () => {
setShowDropDownMenu(!showDropDownMenu);
};
// 로그아웃
const handleLogout = () => {
logout();
};
return (
<div className="header">
<div className="header_container">
{/* 로고를 클릭하면 메인 페이지(/)로 이동 */}
<NavLink to="/">
<img className="header_logo" src={layoutLogo} alt="" />
</NavLink>
<ul className="header_nav">
<li>
<NavLink
to="/matching-list"
style={{
color: location.pathname.startsWith('/matching-list') ? 'var(--color-blue)' : 'var(--color-black)',
fontWeight: location.pathname.startsWith('/matching-list') ? 'bold' : '500'
}}
>
매칭
</NavLink>
</li>
<li>
<NavLink
to="/adoption"
style={{
color: location.pathname.startsWith('/adoption') ? 'var(--color-blue)' : 'var(--color-black)',
fontWeight: location.pathname.startsWith('/adoption') ? 'bold' : '500'
}}
>
입양공고
</NavLink>
</li>
<li>
<NavLink
to="/board/list"
style={{
color: location.pathname.startsWith('/board') ? 'var(--color-blue)' : 'var(--color-black)',
fontWeight: location.pathname.startsWith('/board') ? 'bold' : '500'
}}
>
게시판
</NavLink>
</li>
</ul>
<div className="header_my">
{/* 로그인 여부에 따른 조건부 렌더링 */}
{!isLogin ? (
<button className="header_login_btn" onClick={openLoginModal}>
로그인
</button>
) : (
// 로그인한 상태일 때 사용자 이름과 드롭다운 메뉴 표시
<div className="header_user" onClick={handleShowDropDownMenu}>
<span>{name}님</span>
<img className="header_arrow" src={arrow} alt="" />
{/* 드롭다운 메뉴 표시: role에 따라 조건부 렌더링 */}
{showDropDownMenu && (
<div className="header_dropdown_menu">
{role !== '[ROLE_ADMIN]' ? (
<NavLink to="/mypage">마이페이지</NavLink>
) : (
<NavLink to="/admin/report">신고목록</NavLink>
)}
<NavLink to="/" onClick={handleLogout}>로그아웃</NavLink>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
export default Header;
그리고 애플리케이션이 처음 로드되거나 새로고침될 때, useEffect 훅을 사용해 localStorage에 저장된 사용자 정보를 불러온다.
이 정보를 바탕으로 Zustand의 login 함수를 호출해 사용자의 로그인 상태와 역할을 전역 상태로 복원한다.
이 과정 덕분에 사용자는 새로고침하거나 애플리케이션을 다시 방문해도 로그인 상태가 유지된다.
여기서 찐 문제!!!!
관리자도 URL로 접근하면 /authortiy로 이동한다는 것이다.
src/App.js
function App() {
return (
<Router>
<Layout>
<ScrollToTop />
<Routes>
<Route
path="/admin/report"
element={
<ProtectedRoute requiredRole="[ROLE_ADMIN]">
<ReportList />
</ProtectedRoute>
}
/>
</Routes>
</Layout>
</Router>
);
}
export default App;
App.js의 requiredRole과 localStorage의 role 값이 같음에도 그런 이유를 난 도통 모르겠다,,
일단 localStorage에서는 [ROLE_ADMIN]이라고 막상 콘솔로 찍었을 땐 다를 수도(?) 있으니 한번 찍어보자
콘솔에도 정확히!! [ROLE_ADMIN] 인걸 확인했다.
그럼 Zustand의 상태 초기화 문제
콘솔에 [ROLE_ADMIN] [ROLE_ADMIN]이 찍혔다.
그 말은 role과 requiredRole이 같다는 것이고 /authority로 넘어가는게 아닌 /admin/report로 넘어가야 한다는 것이다.
근데 왜 /authority로 넘어갈까
해결방법
https://chaereemee.tistory.com/23
⚡️ 트러블 슈팅 - 지연 초기화
문제 식별관리자로 로그인했음에도 불구하고, 권한이 없는 페이지로 리다이렉트되는 문제가 발생했다.role과 requiredRole이 [ROLE_ADMIN]으로 동일하게 출력되고 있다면,일반적인 상황에서는 if 조건
chaereemee.tistory.com
'팀프로젝트_PetHarmony' 카테고리의 다른 글
Spring Security 기록 (JWT 만료 처리 전) (2) | 2024.09.04 |
---|---|
⚡️ 트러블 슈팅 - 지연 초기화 (0) | 2024.09.02 |
🌈 예외처리 (1) | 2024.08.25 |
🌈 React 공통 컴포넌트 커스텀 CSS (1) | 2024.08.23 |
🌈 컴포넌트 간 UI 동기화 (0) | 2024.08.23 |